前端性能优化:用 requestIdleCallback 把长任务拆成小片
页面卡顿,很多时候不是代码写得慢,而是主线程一次干得太多。一个超过 50 毫秒的长任务就能让点击、滚动、输入出现可感知的延迟;如果页面在启动时还要解析几百条数据、渲染一个长列表,用户看到的就是“点一下,要等一会儿才有反应”。解决思路并不复杂:别一次性做完,把工作切成小片,只在浏览器空闲时慢慢做。requestIdleCallback 就是为这件事准备的 API。
requestIdleCallback 是什么
requestIdleCallback 告诉浏览器:“等你空闲了再跑我这段回调”。浏览器会在一帧的工作完成之后、下一帧开始之前,挑一段空闲时间执行它,并把一个 deadline 对象传进来。deadline.timeRemaining() 告诉你这段空闲还剩多少毫秒,deadline.didTimeout 则表示这次回调是因为超时才被强行执行的。
和 setTimeout 不同,它不会固定在某个时间点触发;和 requestAnimationFrame 不同,它不抢渲染的时间片。它的定位很明确:做那些重要但不紧急的工作。
把长任务切成小片
假设有一个包含 3000 条记录的数组,需要逐条格式化后写入页面。一次性循环 3000 条,主线程可能被堵住几百毫秒。改成小片处理后,思路是这样的:
const tasks = [...]; // 待处理的 3000 条记录
let index = 0;
function runSlice(deadline) {
while (index < tasks.length && deadline.timeRemaining() > 0) {
process(tasks[index++]);
}
if (index < tasks.length) {
requestIdleCallback(runSlice, { timeout: 2000 });
}
}
requestIdleCallback(runSlice, { timeout: 2000 });
每次只处理当前空闲能容纳的几条,处理完就把控制权还给浏览器,下一段空闲再继续。用户在数据处理期间滚动页面、点击按钮,都不会被长任务阻塞。
适合放进空闲时间的任务
并不是所有工作都适合推迟。能放进空闲时间的,一般是“晚一点完成也没关系”的任务:数据预处理、埋点上报的打包、日志聚合、搜索索引的构建、长列表的分批渲染。它们的特点是:用户不依赖结果立刻反馈,工作量可以被切分。反过来,响应用户点击的交互反馈、首屏渲染、动画帧内的计算,就不该交给空闲回调。
两个容易踩的坑
第一个坑是兼容性。requestIdleCallback 在 Chrome、Edge 和 Firefox 中可用,但 Safari 至今没有实现。正式使用时要准备降级:没有这个 API 的浏览器,用 setTimeout 分片执行,或者直接用调度器库(React 的 Scheduler 就是类似思路)。可以写一个很小的兼容封装:
const idle = window.requestIdleCallback
? (fn, opts) => requestIdleCallback(fn, opts)
: (fn) => setTimeout(() => fn({ timeRemaining: () => 16, didTimeout: false }), 0);
第二个坑是超时时间。timeout 选项的意思是:最多等这么久,到了时间即使不空闲也要执行。它是兜底,不是保证。timeout 设得太短,等于把任务又推回了主线程繁忙期;设得太长,低优先级任务可能一直排不上队。根据任务的重要程度给一个合理的超时值,比如 1000 到 2000 毫秒。
和另外两个 API 怎么选
requestAnimationFrame 是“每一帧开始前执行”,适合动画和界面更新;setTimeout 是“到点执行”,适合固定延迟;requestIdleCallback 是“有空了再执行”,适合后台的批量工作。三者没有优劣,只有分工。把它们用错位置,就会重新制造卡顿:比如在空闲回调里做重排、改样式,会立刻触发渲染,反而吞掉下一帧的时间。
小结
性能优化常常不是把单次执行做得更快,而是把执行的时间排布得更合理。用 requestIdleCallback 把长任务拆成小片,核心就三句话:能切分的工作不要一次性做完;只在浏览器的空闲时间里做;为不支持的浏览器准备降级方案。页面少了几百毫秒的长任务,用户最直观的感受就是:点哪里,哪里立刻有反应。这就是它值得写进代码的原因。
- 点赞
- 收藏
- 关注作者
评论(0)