Worker 布局指南
启用异步布局、取消旧任务,并检查 Worker 回退与部署资源
Worker 可以把布局计算放到后台线程,让画布在计算期间保持响应。布局类型和算法参数见布局通用配置项。
启用方式
布局是否尝试在后台计算,由 layout.worker 设置。
layout: { type: 'force-atlas2', worker: true, maxIteration: 100 },| layout.worker | Graph 中的行为 |
|---|---|
| 省略 | 节点数 ≥ 1000 时尝试启用;低于 1000 时走主线程 |
| true | 对库布局尝试 Worker;注册扩展与三个内置树布局仍优先在主线程执行 |
| false | 显式关闭,包括大图 |
独立 runLayout 没有按节点数自动启用的行为。Worker 失败会回退到同一算法的主线程实现;具体执行顺序和默认迭代值见布局参考。
代码与对应效果
先显示 160 个节点的网格。点击 Run Worker 请求 ForceAtlas2;点击 Grid 恢复网格。Start + cancel 启动较长任务,20ms 后取消并重新排列为网格;最终应保持 cancelled → grid ready,不被旧结果覆盖。
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();自行创建的按钮、计时器和订阅也要清理。
部署与确认
- npm ESM 产物包含 layoutWorker.js 及其依赖,部署时保留文件相对关系;不要只复制 index.js。
- 构建器需识别模块 Worker 的 new URL 入口;检查 Network 中 Worker 及算法资源请求成功、响应是 JavaScript,而非站点的 HTML 回退页。
- 在开发工具的线程列表中确认 Worker;如业务受 CSP 约束,按实际资源来源配置 worker-src 和脚本策略,不要仅以布局成功判断。
- 当前 UMD 入口不保证独立 Worker 资源可用,失败时会回退;需要稳定离线程计算时优先使用 ESM 接入并验证构建产物。
常见问题
启用后仍然卡顿?
先确认是否回退到主线程,再分别测量计算和绘制。数据传输、主线程渲染及大量标签不会因 worker 开关消失;控制迭代次数,并使用实际数据测试。
布局回调为什么在小图正常、大图不一样?
大图可能触发自动 Worker。layout 配置中的函数无法克隆,导致回退;需要回调时显式关闭 Worker,使执行路径稳定。
cancelLayout 会抛出 AbortError 吗?
Graph 会放弃被取消任务的结果,正常取消通常不会作为 AbortError 向调用方抛出。完成 Promise 也不表示坐标已写入;应用仍需检查自己的任务代次或销毁状态。
相关可运行示例