使用指南布局

Worker 布局指南

启用异步布局、取消旧任务,并检查 Worker 回退与部署资源

Worker 可以把布局计算放到后台线程,让画布在计算期间保持响应。布局类型和算法参数见布局通用配置项。

启用方式

布局是否尝试在后台计算,由 layout.worker 设置。

layout: { type: 'force-atlas2', worker: true, maxIteration: 100 },
layout.workerGraph 中的行为
省略节点数 ≥ 1000 时尝试启用;低于 1000 时走主线程
true对库布局尝试 Worker;注册扩展与三个内置树布局仍优先在主线程执行
false显式关闭,包括大图

独立 runLayout 没有按节点数自动启用的行为。Worker 失败会回退到同一算法的主线程实现;具体执行顺序和默认迭代值见布局参考。

代码与对应效果

先显示 160 个节点的网格。点击 Run Worker 请求 ForceAtlas2;点击 Grid 恢复网格。Start + cancel 启动较长任务,20ms 后取消并重新排列为网格;最终应保持 cancelled → grid ready,不被旧结果覆盖。

正在加载示例…
运行 Worker 请求,或启动后取消并恢复网格;完成状态不表示未发生主线程回退
完整代码
import { Graph, type LayoutConfig } from '@antv/g6-lite';
import GUI from 'lil-gui';

export async function run(container: HTMLElement) {
  const grid: LayoutConfig = { type: 'grid', rows: 10, cols: 16, width: 400, height: 200, worker: false };
  const graph = new Graph({
    container,
    data: {
      nodes: Array.from({ length: 160 }, (_, i) => ({ id: String(i) })),
      edges: Array.from({ length: 320 }, (_, i) => ({
        source: String(i % 160), target: String((i * 17 + 11) % 160),
      })).filter((edge) => edge.source !== edge.target),
    },
    layout: grid,
    node: { style: { size: 6 } },
    edge: { style: { endArrow: false, opacity: 0.2 } },
    behaviors: [],
    autoFit: { padding: 48, maxZoom: 1 },
  });
  const ui = createDemoPanel(container);
  let generation = 0;
  let timer: ReturnType<typeof setTimeout> | undefined;
  const arrange = async (config: LayoutConfig, name: string) => {
    const current = ++generation;
    ui.setStatus(`${name} running`);
    graph.setLayout(config);
    try {
      await graph.layout();
      if (current !== generation || graph.destroyed) return;
      graph.fitView({ padding: 48, maxZoom: 1 });
      ui.setStatus(`${name} ready`);
    } catch {
      if (current === generation && !graph.destroyed) ui.setStatus('layout failed');
    }
  };
  const force = (maxIteration: number): LayoutConfig => ({
    type: 'force-atlas2', worker: true, maxIteration, barnesHut: true, prune: false,
  });
  const destroy = graph.destroy.bind(graph);
  graph.destroy = () => { generation++; clearTimeout(timer); ui.destroy(); destroy(); };
  try {
    await graph.render();
    ui.setStatus('grid ready');
    ui.addAction('Run Worker', () => {
      clearTimeout(timer);
      void arrange(force(80), 'worker requested');
    });
    ui.addAction('Start + cancel', () => {
      clearTimeout(timer);
      void arrange(force(2000), 'worker requested');
      timer = setTimeout(() => {
        graph.cancelLayout();
        void arrange(grid, 'cancelled → grid');
      }, 20);
    });
    ui.addAction('Grid', () => {
      clearTimeout(timer);
      graph.cancelLayout();
      void arrange(grid, 'grid');
    });
    return graph;
  } catch (error) {
    graph.destroy();
    throw error;
  }
}


export function createDemoPanel(container: string | HTMLElement, description = '') {
  const host = typeof container === 'string' ? document.querySelector<HTMLElement>(container)! : container;
  if (getComputedStyle(host).position === 'static') host.style.position = 'relative';

  let gui: GUI | undefined;
  const getGui = () => {
    if (!gui) {
      gui = new GUI({ container: host, width: 160 });
      gui.domElement.classList.add('g6lite-demo-controls');
      gui.domElement.dataset.captureIgnore = '';
      Object.assign(gui.domElement.style, {
        position: 'absolute',
        top: '10px',
        right: '10px',
        zIndex: '30',
        maxWidth: 'calc(100% - 20px)',
        maxHeight: 'calc(100% - 20px)',
      });
    }
    return gui;
  };

  let status: HTMLDivElement | undefined;
  const setStatus = (message: string) => {
    if (!status) {
      status = document.createElement('div');
      status.setAttribute('role', 'status');
      status.dataset.role = 'status';
      status.dataset.captureIgnore = '';
      status.style.cssText =
        'position:absolute;left:12px;bottom:10px;z-index:20;max-width:min(28rem,calc(100% - 24px));color:#64748b;font:12px/1.45 system-ui,sans-serif;pointer-events:none;white-space:pre-wrap;';
      host.append(status);
    }
    status.textContent = message;
  };
  if (description) setStatus(description);

  return {
    get gui() {
      return getGui();
    },
    addAction(label: string, action: () => void, folder = getGui()) {
      return folder.add({ [label]: action }, label);
    },
    addChoice<T extends string>(
      label: string,
      choices: readonly T[] | Record<string, T>,
      value: T,
      onChange: (value: T) => void,
    ) {
      const options = Array.isArray(choices) ? [...choices] : choices;
      return getGui().add({ value }, 'value', options).name(label).onChange(onChange);
    },
    setStatus,
    destroy() {
      gui?.destroy();
      status?.remove();
    },
  };
}

完成状态只表示本次布局调用完成;要确认实际使用了 Worker,还需检查下方的资源和线程。

传输与回退

传入 Worker 的 layout 配置、节点 data 和边 data 必须可结构化克隆。函数、DOM 节点等值会导致传输失败;需要算法回调时显式设置 worker: false。节点和边的样式函数不在这次传输中,可正常用于绘制。

Worker 仅收到节点 id/x/y/data 和边 source/target/data。Graph 在结果回写时处理 fx/fy;不要依赖未传递字段在 Worker 算法内可用。

创建失败、资源加载失败、postMessage 克隆失败或计算报错,都可能触发主线程回退。配置了 worker: true、触发 layout:end 或 await 正常返回,都不能单独证明实际执行线程。

取消与清理

在渲染完成的实例上调用 graph.cancelLayout(),可终止该实例正在使用的 Worker 并放弃结果;不会回滚已完成布局,也不会影响其他 Graph 的 Worker。继续切换时,重新 setLayout 后 await layout。

示例的 generation 检查还保护应用自己的完成提示和 fitView,避免过期调用结束后继续执行这些副作用。卸载时调用 graph.destroy();自行创建的按钮、计时器和订阅也要清理。

部署与确认

  1. npm ESM 产物包含 layoutWorker.js 及其依赖,部署时保留文件相对关系;不要只复制 index.js。
  2. 构建器需识别模块 Worker 的 new URL 入口;检查 Network 中 Worker 及算法资源请求成功、响应是 JavaScript,而非站点的 HTML 回退页。
  3. 在开发工具的线程列表中确认 Worker;如业务受 CSP 约束,按实际资源来源配置 worker-src 和脚本策略,不要仅以布局成功判断。
  4. 当前 UMD 入口不保证独立 Worker 资源可用,失败时会回退;需要稳定离线程计算时优先使用 ESM 接入并验证构建产物。

常见问题

启用后仍然卡顿?

先确认是否回退到主线程,再分别测量计算和绘制。数据传输、主线程渲染及大量标签不会因 worker 开关消失;控制迭代次数,并使用实际数据测试。

布局回调为什么在小图正常、大图不一样?

大图可能触发自动 Worker。layout 配置中的函数无法克隆,导致回退;需要回调时显式关闭 Worker,使执行路径稳定。

cancelLayout 会抛出 AbortError 吗?

Graph 会放弃被取消任务的结果,正常取消通常不会作为 AbortError 向调用方抛出。完成 Promise 也不表示坐标已写入;应用仍需检查自己的任务代次或销毁状态。

继续阅读:布局指南 · 布局方法 · 性能调优。

相关可运行示例