算法与数据结构 / 动画引擎原理 · 从插值到播放控制 / AnimationWorklet:把动画逻辑搬下主线程 待审核 44 / 47
AnimationWorklet · Houdini · off-main-thread

AnimationWorklet:把动画逻辑搬下主线程

本引擎的 tick 跑在主线程:主线程一忙,rAF 就被推迟,动画掉帧。合成层讲过 transformopacity声明式动画能下到合成器线程、不受主线程阻塞,但那只能表达固定关键帧。

AnimationWorklet 是平台给「让自定义 JS 动画逻辑也跑在合成器线程」的答案:写一个 animate(currentTime, effect) 函数注册成 animator,它在 worklet 线程逐帧运行。

1 · 主线程阻塞时的三条轨

三条轨同时运动:rAF 由主线程逐帧驱动,CSS 那条是声明式 @keyframes 跑在合成器,第三条若支持则由 worklet 线程驱动。

图 1-1 · 三条轨在主线程被同步死循环占满 600 ms 时的表现。顶部徽标显示本浏览器是否启用 AnimationWorklet。

掉帧的根因是「动画逻辑在哪条线程跑」。rAF 与引擎的 tick 都和业务 JS 抢同一条线程;transformopacity 的声明式动画被浏览器下放到合成器线程,所以主线程死循环时它照动。

2 · 一个已经停摆的答案

AnimationWorklet 至今是实验特性,规范长期停滞。Chromium 之外的浏览器不支持,连 Chromium 默认构建也不再暴露它。

警示 · 实测 Chrome 151 的 'animationWorklet' in CSSfalse,而同属 Houdini 的 paintWorklettrue——同一套 Houdini 提案里,绘制那半边落了地,动画这半边没有(核对于 2026-08)。要跑通本页第三条轨需自行开启实验标志;未启用时它降级为一条 CSS compositor 动画占位,与第二条同源。

它的限制也很实在:worklet 线程读不到 DOM,输入受限于时间与滚动等少数几个信号,输出只能是 effect.localTime——即「驱动一个预先声明好的 KeyframeEffect 走到第几毫秒」。能表达的自定义逻辑因此只是「时间到时间」的映射,与时间重映射同构。

所以现实中,绝大多数「不掉帧」的工程手段仍是只动合成属性单 rAF 与读写分离

// worklet 模块 (跑在 worklet 线程, 不能碰 DOM): 自定义 currentTime → 进度
registerAnimator('tri-mover', class {
  animate(currentTime, effect) {
    const phase = (currentTime % 2000) / 2000;
    const tri = phase < 0.5 ? phase * 2 : 2 - phase * 2;  // 自定义逻辑
    effect.localTime = tri * 2000;        // 驱动一个 KeyframeEffect
  }
});

// 主线程: 注册模块, 把 effect 交给 worklet animation
await CSS.animationWorklet.addModule(workletURL);
const effect = new KeyframeEffect(box,
  [{ transform: 'translateX(0)' }, { transform: 'translateX(230px)' }],
  { duration: 2000, fill: 'both' });
new WorkletAnimation('tri-mover', effect, document.timeline).play();
// 此后即便主线程死循环, 这段动画仍在 worklet 线程逐帧运行

为什么 rAF 必须按 delta-time 累加才帧率无关见驱动器与帧;哪些属性能下合成器见合成层;一帧内如何批量读写见调度