什么是 requestIdleCallback?
你有没有写过这种代码:页面一打开,你顺手就想做点“顺便的事”——埋点、预加载、计算一堆东西、顺手还想把用户最近浏览记录也整理一下。
然后你发现:用户的顺便 = 主线程的噩梦。
requestIdleCallback 的思路很朴素:正事(输入、滚动、动画、渲染)优先,剩下的等浏览器“喘口气”再做。像你在工位被人叫住:“哥你先把线上故障处理了,我这 PPT 不急,等你空了再说。”
先一句话:requestIdleCallback 是啥?
requestIdleCallback(cb) 用来注册一个回调:当浏览器主线程出现空闲(idle)时,尽量调用它,让你在“不会影响用户交互”的时间片里做一些不紧急的工作。
几个关键词你得先记住:
- 它是“尽量”,不是“保证”
- 它面向的是主线程的空闲时间(不是 Web Worker 那种真并行)
- 它更适合“可切片、可中断”的任务
为什么会有“空闲”这种东西?
浏览器主线程平时主要忙这些:
- 跑 JS
- 样式计算 / 布局 / 绘制
- 处理输入事件(点击、滚动、键盘)
- 合成与动画调度
如果你在关键路径里塞了一个大计算,用户体验就会变成经典三连:
- 点了没反应
- 滚动像 PPT
- “是不是卡死了?”
而在很多帧之间,或者某些任务结束后,主线程会出现一点点空档期。requestIdleCallback 就是让你把“非关键工作”排队到这些空档里。
API 长啥样?
1 | const id = requestIdleCallback( |
回调会收到一个 IdleDeadline 对象,常用就俩:
deadline.timeRemaining():这次空闲期大概还剩多少毫秒可用deadline.didTimeout:如果你设置了timeout,并且等太久还没空闲,浏览器会“硬执行”,此时它会是true
注意:timeRemaining() 是“估计值”,你别拿它当精密仪器用,它更像“差不多还能摸鱼 10 分钟”那种感觉。
它适合干什么?(以及不适合干什么)
适合
- 埋点/日志上报的“整理工作”(真正发请求还是要注意节流)
- 预计算/缓存:比如把一些数据结构准备好、预生成索引
- 预加载:prefetch 一些可能用到的资源(注意网络别被你薅秃了)
- 大任务切片:把一个很重的计算拆成很多小块分批做
不适合
- 任何“必须立刻完成”的工作(比如首屏关键渲染、关键请求、用户点击后的反馈)
- 严格时序任务(它不保证什么时候执行)
- 需要稳定持续执行的轮询(你会被空闲调度搞得心态爆炸)
最常见也最实用的用法:把大任务切成小块
你可以把 requestIdleCallback 理解成“给你一个小时间片”,那你的任务就要学会“见好就收”。
比如你要处理一万个数据项,直接 for 循环跑完,主线程可能会直接翻白眼。更聪明的做法是:每次空闲处理一部分,时间不够就先停。
1 | function processInIdle(tasks, handler, options) { |
这里的关键是这句:
deadline.timeRemaining() > 5
意思是“别把时间片吃干抹净,给浏览器留点缓冲”。你也可以根据任务复杂度调整阈值。
timeout:给它一个“最迟期限”
一个很现实的问题:如果页面一直很忙(比如动画多、输入多、任务多),空闲期可能很少,甚至你注册的回调一直不触发。
这时候 timeout 就像你在群里 @ 一句:“有空帮我看下”没人理,然后你过两分钟直接打电话。
1 | requestIdleCallback( |
经验上:
- 你希望“最终一定要做”的事情(比如缓存清理),可以给个 timeout
- 你希望“完全不影响用户体验”的事情(比如某些预计算),可以不设或设更长
取消:别让它变成“过期任务”
有些任务是“此时此刻需要”,过两秒可能就没意义了,比如用户已经离开页面了。
所以在组件卸载、路由切换、tab 不可见时,记得取消:
1 | let idleId = null; |
兼容性:别把它当成“人人都有”
requestIdleCallback 并不是所有浏览器都支持,尤其在 Safari / iOS 生态里经常踩坑。
所以更稳的写法是做一个降级:
1 | const requestIdle = window.requestIdleCallback |
这个 fallback 很“糙”,但它至少保证了:
- 不会因为 API 不存在直接报错
- 逻辑还能跑,只是“空闲调度”变成了“尽快安排”
如果你特别在意跨端一致性,可以考虑引入 polyfill(但别为了一个小功能把包体拉大到离谱)。
常见坑:你以为空闲,其实浏览器没空
1) 任务永远没机会跑
页面一直忙,回调一直不触发。解决思路:
- 给
timeout - 降低任务强度:切片更小、每次处理更少
- 重新评估:这活是不是该放 Worker 里?
2) 回调里干了“重活”,还是会卡
requestIdleCallback 不是免死金牌。它只是把你安排在“可能空闲”的时间段,但如果你一次干 30ms,用户还是会感觉卡。
这就是为什么“切片”很重要。
3) 在回调里疯狂读写 DOM
回调里频繁触发布局(比如读 offsetHeight、写 style,再读 layout)会把浏览器搞得更忙。
建议:
- 尽量做纯计算/数据整理
- DOM 更新要做也分批,别一口气把布局系统掀翻
结尾:怎么用才算“有用但不作妖”?
如果你只记三条,我建议是:
- 把它当成“后台排队”,别当成“准时闹钟”
- 任务要可中断、可切片,永远别在里面憋大招
- 需要最终保证执行的,加
timeout,并做好降级
等你把这套用顺了,你会发现页面性能优化里有个很现实的境界:不是把主线程榨干,而是让它有呼吸感。
Happy Coding!
