别再给主线程塞私活了!requestIdleCallback 让你优雅“偷懒”
引言“我们页面加载完还要上报用户行为、预加载下一屏数据、提前解析埋点配置、顺便把离线包也更新一下……”产品经理指着需求文档一脸真诚地看着我“这些都是必须做的不影响首屏吧”我点点头“不影响都放 setTimeout 里延迟执行就行。”一个月后线上用户反馈滚动页面总感觉“一卡一卡”的尤其是点开大图的时候浏览器像喝了假酒。我打开 Performance 面板好家伙主线程的“任务清单”比我的购物车还长——原来那些“不重要的”延迟任务全都在用户滚动时跑出来抢 CPU 了。那天下午我偶然发现了一个叫requestIdleCallback的 API它让我学会了什么叫“让浏览器有空了再干活”。一、问题的本质setTimeout 的“自私”行为setTimeout(fn, 0)是我们常用的“让任务稍后执行”的办法。但它有一个问题它只管在延迟结束后把任务塞进宏任务队列不管浏览器现在忙不忙。如果用户正在滚动页面每帧只有 16ms 的处理时间而setTimeout里有一个耗时 50ms 的计算任务就会直接挤占渲染时间造成掉帧、卡顿。换句话说setTimeout是个“不分场合”的积极员工——不管你忙不忙它都要抢着干活。而requestIdleCallback则完全不同它会在浏览器主线程空闲的时候才执行你的任务。就像一个体贴的同事看到你在忙就说“不急你先忙完我等你。”二、初识 requestIdleCallback浏览器的“空闲时间”2.1 基本用法requestIdleCallback(myIdleTask,{timeout:2000});functionmyIdleTask(deadline){// deadline.timeRemaining() 返回当前帧还剩余多少毫秒while(deadline.timeRemaining()0tasks.length0){doOneTask();// 每次做一小块工作}// 如果还有没做完的任务再注册一次空闲回调if(tasks.length0){requestIdleCallback(myIdleTask);}}requestIdleCallback接收两个参数回调函数当空闲时执行会传入一个IdleDeadline对象。可选配置{ timeout: ms }表示即使一直没有空闲时间最多等待ms毫秒后也强制执行。IdleDeadline对象有两个关键成员timeRemaining()返回当前帧还剩多少毫秒一般不超过 50ms。didTimeout布尔值表示是否因为timeout而强制触发。2.2 应用场景非关键数据的预加载如用户可能不会马上用到的内容埋点上报不急于立刻发出去的统计预渲染下一页的模板离线缓存更新任何可以延后、分时执行的任务三、实战优化一个“滥用 setTimeout”的页面3.1 优化前塞满主线程的 setTimeout// 页面加载后要执行一大堆“不重要”的任务window.addEventListener(load,(){setTimeout((){// 解析埋点配置50msparseAnalyticsConfig();},0);setTimeout((){// 预加载下一屏图片20mspreloadNextImages();},0);setTimeout((){// 检查版本更新30mscheckForUpdates();},0);});这些任务虽然都“延迟”到了 load 之后但它们仍然会在同一帧内连续执行因为setTimeout0 的回调会在下一个宏任务里依次执行导致用户刚看到页面一滚动就卡。3.2 优化后用 requestIdleCallback 分散执行window.addEventListener(load,(){consttasks[parseAnalyticsConfig,preloadNextImages,checkForUpdates,// ... 更多];functionrunTasks(deadline){// 当还有剩余时间且还有任务时执行任务while(deadline.timeRemaining()0tasks.length0){consttasktasks.shift();task();}// 如果还有任务没做完继续请求空闲回调if(tasks.length0){requestIdleCallback(runTasks,{timeout:5000});}}requestIdleCallback(runTasks,{timeout:5000});});这样一来每个空闲时间段只执行一小部分任务不会长时间霸占主线程滚动体验瞬间流畅。四、深入原理什么是“空闲时间”浏览器以帧为单位进行渲染通常 60fps每帧约 16.6ms。每一帧的工作流程大致是处理用户输入点击、滚动等执行 JavaScript 任务宏任务执行 requestAnimationFrame 回调布局、绘制、合成空闲时间如果还有剩余时间 16.6ms就执行requestIdleCallback任务如果某一帧没有空闲时间比如 JavaScript 任务耗时过长空闲回调就会被推迟到下一帧直到有空闲为止。因此requestIdleCallback不会影响关键渲染和交互是真正的“后台任务”。五、注意事项与常见陷阱5.1 不要在里面做 DOM 修改如果在空闲回调里修改 DOM可能会触发额外的回流和重绘浪费宝贵的空闲时间。尽量只做数据处理、存储操作等不涉及可视区域变化的任务。5.2 不要假设能执行完所有任务timeRemaining()通常最多给你 50ms各浏览器实现略有差异。你的任务必须能被切分成小段每次执行一小部分否则可能阻塞后续空闲回调。5.3 低优先级任务才适合不能把关键渲染逻辑放进去比如动画更新因为可能延迟很久才执行。如果你有一个任务必须在 1 秒内完成一定要设置timeout兜底。5.4 兼容性requestIdleCallback在 Safari 和 iOS 上不支持。可以用setTimeout做降级也可以利用requestAnimationFrameperformance.now模拟一个简单的空闲检测。不过随着苹果的跟进未来应该会全面支持。降级示例constrequestIdleCallbackwindow.requestIdleCallback||function(cb,options){conststartDate.now();setTimeout((){cb({timeRemaining:()Math.max(0,50-(Date.now()-start)),didTimeout:false});},0);};六、与其他 API 的对比API执行时机适用场景setTimeout延迟一段时间后无论忙闲延迟执行但无空闲控制requestAnimationFrame下次重绘前动画、DOM 同步更新requestIdleCallback浏览器空闲时后台非关键任务setImmediateNode事件循环的下一个阶段类似但只在 Node 中有七、总结做一个“懂礼貌”的程序员requestIdleCallback不是万能的但它给了我们一个优雅的方式去处理那些“不紧急但必须做”的任务。它就像一位情商高的同事总是在大家不忙的时候才来请求帮助既完成了工作又不打扰他人。下次当你想把一堆非关键任务塞进setTimeout的时候不妨问问自己“这些任务能不能等浏览器闲了再做”如果可以就用requestIdleCallback让你的页面更流畅。每日一问如果你有一个非常耗时的计算任务必须执行但又不能卡顿页面除了用 Web Worker你会如何结合requestIdleCallback来分片执行评论区说说你的方案
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2436701.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!