将关键帧插值计算移至 worker 是守住 60fps 最直接有效的做法,核心是让动画时间推演与数值生成脱离渲染链路,仅将确定的可绘制结果交还主线程。

把关键帧插值计算从主线程挪到 Worker,是守住 60FPS 最直接有效的做法。核心不是“多开一个线程”,而是让动画的时间推演和数值生成彻底脱离渲染链路,只把确定的、可绘制的结果交还给主线程。
哪些关键帧计算适合移进 Worker
不是所有动画逻辑都值得迁移。重点处理以下三类高开销、纯数学、无 DOM 依赖的任务:
- 时间线插值:比如 Theatre 中的贝塞尔缓动、分段线性插值、自定义 easing 函数(如 cubic-bezier(0.25, 0.46, 0.45, 0.94))在任意 progress 值下的输出计算
- 路径点生成:mojs 的 motion-path 或 SVG path 动画中,将控制点序列展开为数百个采样点(如 1000 段贝塞尔曲线细分),全程不涉及 DOM 或 Canvas
- 多属性同步插值:当一个动画同时驱动 opacity、scale、rotate、x/y 位移时,Worker 可一次性算出整组数值,避免主线程反复调用多个插值函数
数据怎么传才高效
Worker 和主线程之间不能共享内存或函数引用,必须靠 postMessage 通信。关键原则是:只传结构化数据,禁用任何运行时上下文。
-
支持传入的类型:数字、字符串、普通对象、数组、
Float32Array、ArrayBuffer、Date -
禁止传入的类型:函数、class 实例(如
KeyframeEffect)、Promise、正则表达式、DOM 节点、Canvas 上下文 -
大数组务必零拷贝:若插值结果含上万个坐标点,主线程应创建
new Float32Array(20000).buffer,再用worker.postMessage({ type: 'keyframes', data: buffer }, [buffer])—— 第二个参数声明 transferable,Worker 内直接复用,无内存复制开销
如何避免卡顿与掉帧
单次计算耗时超过 16ms 就可能挤占一帧。Worker 不是“万能解药”,必须配合主动切片和进度反馈:
-
分块计算 + 进度上报:例如对 5000 帧做插值,按每 250 帧为一块;Worker 每完成一块就发
{ type: 'progress', done: 250 },主线程可据此更新加载状态或启用骨架预览 -
结果聚合在 Worker 内完成:不要返回 20 个中间数组让主线程合并;Worker 应负责归并、去重、格式标准化,最终只发一个轻量对象:
{ frames: Float32Array, timestamp: 123456789 } -
任务结束立即销毁:主线程收到
type: 'result'后,立刻执行worker.terminate();Worker 自身也可在最后调用self.close(),防止空转占用资源
实际协作流程示意
以 Theatre 编辑器拖拽关键帧后触发重计算为例:
- 主线程收集所有关键帧时间戳、属性值、插值类型,打包为纯数据对象
- 发给 Worker:
{ type: 'interpolate', tracks: [{ prop: 'x', keys: [[0,100],[0.5,200],[1,150]] }] } - Worker 解析、逐轨道插值、生成完整帧序列(如 60fps × 10s = 600 帧),附带当前时间戳
- 主线程收到后,不做二次加工,直接喂给渲染层(如 OffscreenCanvas 或 requestAnimationFrame 回调)











