为了提高浏览器的性能和用户体验,现代浏览器采用了一些技术来优化主线程的执行。主要有:
- setTimeout 延迟加载 (使用不当可能适得其反)
- web worker 多线程 (不能操作dom,主要计算密集型的任务)
- requestIdleCallback 帧空闲时运行 (react18带🔥的,低优先级任务)
- requestAnimationFrame 每帧都会运行 (主要做动画效果调优)
setTimeout 延迟加载非常常见,但是它有很多弊端,而且延迟时间并不准确。最佳的替代品应该是requestAnimationFrame,他的延迟时间要比setTimeout更精准,使用起来和setTimeout类同,只是不需要传延迟时间而已。
web worker在性能优化上经常用到,但是它不能操作dom,可以用来处理繁琐的计算逻辑。
requestIdleCallback它的目的是不要让浏览器再摸鱼了,赶紧起来干活。
浏览器的渲染进程的主线程,每秒重绘60帧,每一帧需要处理很多任务,除了html解析,还有开发者在JS中定义的其他任务,如微任务、元素事件任务、延时任务等。一般来说开发者可以自定义任务的执行时机,以前的时候都是利用setTimeout来通过重启task来改变任务执行时机,主线程一旦被阻塞,他的执行就会被无限延迟。体验非常狼狈。
浏览器放出2过分api:requestanimationframe和requestIdleCallback,他们的执行时机,完全由浏览器控制。
可以看出每个任务执行的时机确实是由浏览器自行决定的,这种方法适用于处理和渲染无关的低优先级事件。
调用时机:
requestanimationframe调用于下一帧重绘之前。
requestIdleCallback则略显复杂,分两种情况:
- . 若当前帧有足够的空闲时间就调用,否则等下一帧
- . 若当前帧没有足够的空闲时间,但是等待时间已经超时了,也会立即调用
react的fiber核心就是requestIdleCallback实现的。
1<!DOCTYPE html>
2<html>
3 <body>
4 <span>js耗时任务测试</span>
5 <script>
6
7 function processTaskTime(callback, ...args) {
8 const startTime = new Date().valueOf();
9 callback(...args);
10 let endTime = new Date().valueOf();
11 const processTime = endTime - startTime;
12 console.log(`%c 执行 ${callback.name}(${args}) 消耗时间:` + processTime + '毫秒', 'color:green')
13 return processTime;
14 }
15
16
17 function sumLoop(start, end) {
18 let sum = 0
19 for (let i = start; i < end; i++) {
20 for (let j = start; j < end; j++) {
21 tmp = sum;
22 sum = tmp + i + j
23 }
24 }
25 console.log(`从${start}到${end}自相相加和为:` + sum)
26 return sum
27 }
28
29 processTaskTime(sumLoop, 0, 15000)
30 </script>
31 <button onclick="processTaskTime(sumLoop,0,15000)"> 执行耗时任务 </button>
32 <button onclick="javascript:console.log('click event')"> click </button>
33 </body>
34</html>
35
页面加载半天,才能出现结果。这就是典型的长任务,我们优化的目标。
第一种优化方案setTimeout
1processTaskTime(sumLoop, 0, 15000);
2 改 ⇓ 写
3setTimeout(()=>processTaskTime(sumLoop, 0, 15000),100)
4
执行速度感觉确实快了一点
第二种优化方案Web Worker
1
2
3
4self.onmessage = function(event) {
5
6 var data = event.data;
7
8
9 var result = performCalculation(data);
10
11
12 self.postMessage(result);
13};
14
15function performCalculation(data) {
16
17
18
19 return data * 2;
20}
21
主线程
1
2var worker = new Worker('worker.js');
3
4
5worker.onmessage = function(event) {
6
7 var result = event.data;
8
9
10 console.log('计算结果:', result);
11};
12
13
14var data = 5;
15worker.postMessage(data);
16
主线程里面用 new Worker 创建 work 实例,然后在构造函数里面传入子线程执行文件的地址,这样子线程就可以和主线程通信了,利用onmessage和postmessage的方法相互传递数据。
页面加载速度得到了明显的提升。
第三种优化方案requestIdleCallback
requestIdleCallback
是一个Web API,允许开发者在主线程空闲时去执行低优先级回调函数。这个函数的主要目的是使得开发者能够在不影响关键事件如动画和输入响应的情况下,执行后台或低优先级的任务。
不是每一帧都会执行,只有在浏览器主线程空闲的时候才会执行。
变成了秒出现页面。
第四种优化方案 requestAnimationFrame
1<!DOCTYPE html>
2<html>
3 <body>
4 <span>js耗时任务测试</span>
5 <script>
6
7 function processTaskTime(callback, ...args) {
8 const startTime = new Date().valueOf();
9 callback(...args);
10 let endTime = new Date().valueOf();
11 const processTime = endTime - startTime;
12 console.log(`%c 执行 ${callback.name}(${args}) 消耗时间:` + processTime + '毫秒', 'color:green')
13 return processTime;
14 }
15
16
17 function sumLoop(start, end) {
18 let sum = 0
19 for (let i = start; i < end; i++) {
20 for (let j = start; j < end; j++) {
21 tmp = sum;
22 sum = tmp + i + j
23 }
24 }
25 console.log(`从${start}到${end}自相相加和为:` + sum)
26 return sum
27 }
28
29
30 requestAnimationFrame(()=>{
31 processTaskTime(sumLoop, 0, 15000)
32 })
33
34
35 </script>
36 <button onclick="processTaskTime(sumLoop,0,15000)"> 执行耗时任务 </button>
37 <button onclick="javascript:console.log('click event')"> click </button>
38 </body>
39</html>
40
执行代码以后,你会发现他的加载速度变成了4个里面最慢的哪一个,是不是很诧异?
setTimeout
requestIdleCallback
requestAnimationFrame
他们三个都重新启动了宏任务
requestIdleCallback 注册的回调函数时,该回调函数会作为一个宏任务被添加到事件队列中。 requestIdleCallback 的回调函数在执行时是依赖于事件队列的管理的。当浏览器在空闲时段时调用。
主线程空闲的时候,说明事件队列其他的(与requestIdleCallback
无关的)回调已经执行完了,requestIdleCallback
就处于事件队列最前面。
使用场景
1️⃣埋点日志相关
- 在用户有操作行为时(如点击按钮、滚动页面)进行数据分析并上报
- 处理数据时往往会调用 JSON.stringify ,如果数据量较大,可能会有性能问题。
1const queues = [];
2
3document.querySelectorAll('button').forEach(btn => {
4 btn.addEventListener('click', e => {
5
6 pushQueue({
7 type: 'click'
8
9 }));
10
11 schedule();
12 });
13});
14
15function schedule() {
16 requestIdleCallback(deadline => {
17 while (deadline.timeRemaining() > 1) {
18 const data = queues.pop();
19
20 }
21
22 if (queues.length !== 0) {
23
24 schedule();
25 }
26 });
27}
28
2️⃣预加载
例如当你需要处理一些数据,但这些数据不需要立即展示给用户时,可以在空闲时预处理这些数据
1function prefetch(entry: Entry, opts?: ImportEntryOpts): void {
2 if (!navigator.onLine || isSlowNetwork) {
3
4 return;
5 }
6
7 requestIdleCallback(async () => {
8 const { getExternalScripts, getExternalStyleSheets } = await importEntry(entry, opts);
9
10 requestIdleCallback(getExternalStyleSheets);
11 requestIdleCallback(getExternalScripts);
12 });
13}
14
3️⃣延迟执行
当你有一些非必须立刻执行的代码时,比如初始化某些非关键的UI组件,你可以使用 requestIdleCallback
来推迟这些任务的执行
不适合场景
1️⃣不适合操作dom&更新UI
执行时机不确定可能导致视觉难以预测 空闲回调执行的时候,当前帧已经结束绘制了,所有布局的更新和计算也已经完成。可能会引发回流重绘。 2️⃣不适合做一些耗时的长任务
虽然是在浏览器空闲执行,但依然运行在主线程上,耗时的长任务同样会导致帧率降低, 造成页面卡顿。 requestIdleCallback 不适合执行 DOM 操作,因为修改了 DOM 之后下一帧不一定会触发修改,主线程可能还被占据着。
react 并没有使用了 requestIdleCallback 来解决 stack 的问题,但 react 自主实现的调度算法与 requestIdleCallback 息息相关,那么为什么要放弃它而选择自主实现呢?
浏览器兼容性,目前并不是所有浏览器都支持这个 API 触发频率不稳定 FPS 远低于60, 这远远低于页面流畅度的要求(主要原因)
requestAnimationFrame 是一个浏览器提供的 JavaScript 方法,用于优化执行动画和其他循环操作的效率。它允许开发者在浏览器的下一次重绘之前调度一个回调函数,以确保动画在每一帧中都能够以最佳的性能和流畅度运行。
1function animate() {
2
3
4
5 requestAnimationFrame(animate);
6}
7
8
9requestAnimationFrame(animate);
10
使用场景
⭐ 当使用 setInterval 或 setTimeout 来执行循环操作或动画时,存在以下问题:
1️⃣不稳定的帧率:setInterval 和 setTimeout 方法是按照指定的时间间隔执行回调函数。然而,浏览器的重绘率(屏幕刷新率)通常是固定的,例如 60 Hz(每秒 60 帧)。如果指定的时间间隔小于重绘率,那么某些帧可能会被跳过,导致动画不连续或不流畅。反之,如果时间间隔大于重绘率,动画可能会显得卡顿。
2️⃣不可预测的性能:使用 setInterval 或 setTimeout 无法准确控制每一帧的执行时间。由于 JavaScript 是单线程的,如果在某一帧执行的回调函数需要较长时间来完成,那么下一帧的回调函数可能会被延迟执行,从而导致不稳定的性能表现。这可能会导致动画的延迟、卡顿或者整体性能下降。
3️⃣响应性差:由于 setInterval 或 setTimeout 是通过定时器触发回调函数,它们不考虑浏览器的渲染过程。这意味着即使浏览器当前正在进行重绘,回调函数也会被触发。这可能导致在关键渲染时刻执行 JavaScript 代码,从而影响页面的响应性能。
⭐ 相比之下,requestAnimationFrame 是专门为动画优化而设计的方法,解决了上述问题:
1️⃣平滑的帧率:requestAnimationFrame 的回调函数会在每一帧开始绘制之前被调用,与浏览器的重绘率同步。这意味着动画将以流畅的 60 帧/秒(或其他重绘率)运行,产生连续而平滑的动画效果。
2️⃣更好的性能控制:requestAnimationFrame 的回调函数会在浏览器准备好绘制下一帧时被调用,确保每一帧的执行时间在可接受范围内。这有助于提供更稳定的性能,避免过长的回调导致的性能问题。
3️⃣更好的响应性能:requestAnimationFrame 会自动与浏览器的渲染过程同步。如果页面不可见或最小化,requestAnimationFrame 将自动暂停,避免不必要的计算和功耗。这对于提高页面的响应性能和用户体验非常重要。
1<!DOCTYPE html>
2<html>
3 <body>
4 <div id="box" style="height: 20px; width: 20px; background-color: blue;position: relative;"></div>
5 <script>
6 const box = document.querySelector('#box');
7 let position = 0;
8 const speed = 20;
9
10 function animate() {
11
12 position += speed;
13
14 box.style.left = position + 'px';
15 if (position < 500) {
16 window.requestAnimationFrame(animate)
17 }
18 }
19
20 window.requestAnimationFrame(animate)
21 </script>
22 </body>
23</html>
24
1 const [status,setStatus]=useState(0);
2 function splitTaskRender(){
3 setStatus(++status)
4 if(status<3){
5 window.requestAnimationFrame(splitTaskRender)
6 }
7 }
8 window.requestAnimationFrame(splitTaskRender)
9
10 {status>1&&<div>Module 1</div>}
11 {status>2&&<div>Module 2</div>}
12 {status>3&&<div>Module 3</div>}
13
性能问题 requestAnimationFrame 回调函数中执行的任务太耗时,会导致以下问题:
1️⃣卡顿和掉帧:如果回调函数的执行时间超过了每帧的时间限制(通常为 16.7 毫秒),就会导致浏览器无法及时完成绘制,导致页面出现卡顿和掉帧的现象。
2️⃣资源占用过多:执行耗时任务可能会占用过多的 CPU 资源,导致浏览器的性能下降。这可能会影响其他页面元素的响应性能,导致页面变得不流畅,用户可能无法顺利地与页面进行交互。
优化策略 1️⃣分割任务:将耗时任务分割为多个较小的子任务,并在多个帧中逐步执行。这样可以避免单个帧中执行时间过长,减少卡顿和掉帧的问题。(参考上文 首屏多模块加载优化)
2️⃣优化算法:通过优化算法或使用更高效的数据结构,尽量减少任务的执行时间。
3️⃣降低帧率:如果任务确实无法在单个帧中完成,可以通过降低 requestAnimationFrame 的调用频率,减少任务的执行次数,以达到平衡可接受的性能和任务完成的目标。
1let frameCount = 0;
2const frameInterval = 10;
3
4function animate() {
5 frameCount++;
6
7 if (frameCount % frameInterval === 0) {
8
9
10
11
12 frameCount = 0;
13 }
14
15 requestAnimationFrame(animate);
16}
17
18requestAnimationFrame(animate);
4️⃣使用 Web Worker:对于非UI相关的耗时任务,可以考虑使用 Web Worker 在后台线程中执行,以充分利用多线程并减少对主线程的影响。