web worker 与 webgl 渲染线程分离的本质是将重计算、重准备任务移出主线程,使其专注绘制和交互;worker 负责模型解析、物理模拟、资源预处理,不共享 webgl 对象,通信仅限结构化数据与可转移 arraybuffer。

Web Worker 与 WebGL 的渲染线程分离,本质不是“让 Worker 去画图”,而是把原本挤在主线程里的重计算、重准备任务搬出去,腾出主线程专注做 WebGL 绘制和用户交互。关键在于职责划清、数据闭环、上下文隔离。
明确分工:谁该做什么
主线程保留不可替代的职责:创建 OffscreenCanvas、获取 WebGL 上下文、执行 drawArrays/drawElements、响应鼠标/键盘事件、管理帧循环(requestAnimationFrame)。Worker 则只做三类事:模型解析与顶点生成、物理模拟与状态更新、纹理/着色器资源预处理。两者不共享 WebGL 对象,也不互相调用 gl 方法。
- 矢量瓦片解析(如 Mapbox GL JS)→ Worker 解包 PBF、按样式生成 Bucket 缓冲数据,再传回主线程绑定 VBO
- 力导向图布局计算(如 react-force-graph)→ Worker 迭代节点位置,只传坐标数组,不碰 Canvas
- 热力图像素合成 → Worker 构建 ImageData,主线程用 ctx.putImageData 渲染
资源加载必须闭环在 Worker 内
Worker 中不能用 new Image() 或 document.createElement,也不能调用 URL.createObjectURL()。所有静态资源加载必须走 fetch + createImageBitmap 流程,纹理上传必须用 gl.texImage2D,且整个链条(fetch → decode → upload → bind)必须在同一个 Worker 内完成。OffscreenCanvas 实例需由主线程创建后通过 postMessage 传入,Worker 拿到后立刻调用 getContext('webgl2'),不能延迟。
- 错误做法:Worker 里 fetch 图片后发给主线程,再由主线程 new Image().onload → 触发跨线程回退,卡顿
- 正确做法:Worker fetch → createImageBitmap → gl.texImage2D → gl.generateMipmap → postMessage({ textureId: id, width, height })
- 着色器源码必须 fetch 获取文本,不能读取 <script type="x-shader"> 标签</script>
通信设计要轻量、结构化
避免传大对象或 WebGL 原生对象(如 Texture、Buffer),只传纯 JSON 数据或可转移的 ArrayBuffer。推荐使用版本号或哈希标识资源状态,Worker 完成后回传 success + 元数据(尺寸、格式、校验值),主线程据此决定是否触发绘制。
- 物理模拟结果:传 { time: 12345, positions: Float32Array.buffer, velocities: Float32Array.buffer }
- 瓦片解析结果:传 { tileId: 'z12-x34-y56', buckets: [ { layer: 'road', vertices: ..., indices: ... } ] }
- 禁止传 gl 对象、TypedArray 实例本身(除非 transferable)、DOM 节点
性能落地的关键细节
Worker 不是万能加速器,用不好反而拖慢。需配合线程池管理(如 MapLibre 的 WorkerPool)、任务批处理、缓存策略(Cache API 配合 Service Worker)、以及 WebGL2 特性(如 transform feedback、compute shader)协同优化。preserveDrawingBuffer 必须设为 false;截图靠 gl.readPixels + transferToImageBitmap,不用 toDataURL。
- 小任务别开新 Worker,复用已有线程池
- 跨域资源确保服务端返回 Access-Control-Allow-Origin: *
- 大量纹理上传建议分帧提交,避免单次 GPU 队列过载
- WebGL2 计算着色器适合在 Worker 中预处理顶点数据,再传回主线程绘制











