动画跳帧你以为是代码写得烂?根子其实不在代码——scheduler.wait() 把主线程时序控制还给你了
动画跳帧你以为是代码写得烂?根子其实不在代码,在主线程的时间片管理上——Chrome 126 引入的 scheduler.wait(),把这件事彻底变了。
事情是这样的:你在做一个逐帧动画,每帧预算 16ms,但你的某个处理函数逻辑比较重,有时候跑完已经花了 14ms,你不知道该不该继续跑下一个子任务——跑的话可能超帧预算,不跑的话帧率又上不去。以前的解法是用 setTimeout(fn, 0) 硬切,但 setTimeout(fn, 0) 切完回来的时候,你的任务已经排到队列最后了,前面可能排着一堆其他任务,真正回来的时候帧预算早就超了。
Chrome 126 的 scheduler.wait() 就是来解决这个问题的。它是 scheduler.yield() 的进阶版——yield 是让出主线程让别人先跑,wait 则是让出主线程、但给自己设置一个「截止时间」,在这个截止时间之前如果主线程有空了就立刻回来,如果主线程一直忙着就等到截止时间到了强制回来。
async function processFrame() {
const start = performance.now();
// 子任务1
await doTask1();
const t1 = performance.now();
await scheduler.wait(Math.max(1, 16 – (t1 – start)));
// 子任务2
await doTask2();
const t2 = performance.now();
await scheduler.wait(Math.max(1, 16 – (t2 – start)));
// 子任务3
await doTask3();
}
每个子任务跑完之后,算一下帧预算还剩多少,然后调用 scheduler.wait() 等待剩余时间。如果主线程在剩余时间内空了,任务立刻回来继续执行下一帧,不会多等;如果主线程一直被其他任务占着,等到截止时间到了也会强制回来,不会无限期地被打断。
setTimeout(fn, 0) 为什么不香
setTimeout(fn, 0) 也能切分长任务,但有个问题:切完之后你的任务续作被排到了队列最后。所有在这 0ms 内新进来的任务——第三方 analytics 回调、其他组件的更新、另一个 setTimeout——全排在你的任务前面。结果就是你的循环被其他你不关心的工作堵住,本来 100ms 能跑完的任务拖到 300ms 还跑不完。
scheduler.wait() 续作会被放进优先队列,浏览器回来的时候会优先执行你的任务,而不是排在队列最后。用 Chrome 的话说,scheduler.wait() 的续作优先级高于同等优先级的 scheduler.postTask() 任务,所以你的动画循环不会被其他任务饿死。
用 INP 的视角看这件事
INP(Interaction to Next Paint)衡量的是用户点击到下一次渲染完成之间的延迟。INP = 输入延迟 + 处理时间 + 呈现延迟,其中输入延迟就是主线程被其他任务占着、你的交互要等多久才能开始处理。如果你的页面上有个动画一直在跑,主线程被占着,用户这时候点击一个按钮,按钮的 handler 要等动画任务让路才能开始——这就是输入延迟。
scheduler.wait() 让动画任务在每个帧预算边界主动让路,而不是等操作系统强制调度。主动让路意味着主线程在让路之后可以立刻处理用户交互,用户感受到的输入延迟就会小很多。
生产环境怎么用
浏览器兼容性:Chrome/Edge 129+(2024 年 9 月),约覆盖全球 70% 流量,Safari 和 Firefox 尚未支持。建议 feature-detection + 降级:
function yieldToMain(timeout) {
if (globalThis.scheduler?.wait) {
return scheduler.wait(timeout);
}
return new Promise((resolve) => setTimeout(resolve, 0));
}
然后在你的动画循环里:
const frameBudget = 16;
const start = performance.now();
await doAnimationFrame();
const remaining = Math.max(1, frameBudget – (performance.now() – start));
await yieldToMain(remaining);
Chrome DevTools Performance 面板里的 Interactions 轨迹可以验证效果——优化前输入延迟的紫色条会很长,优化后主线程让路及时,紫色条会短很多。
下一步:打开 DevTools Performance 面板,录制一次你的页面交互,看 Long Task 占了多宽。然后在动画循环里加 scheduler.wait(),对比前后输入延迟的变化。INP 过了 200ms 的页面,优先处理这个。
评论区
登录后可评论。