一、基本概念

  1. js是单线程执行的,分为GUI渲染线程,js引擎线程,事件触发线程,定时器触发器线程,http请求线程。
  2. 执行js会阻塞gui渲染,如js执行所需时间超过一帧的时间,就会导致页面掉帧,也就是肉眼看到的卡顿。

二、场景模拟

1. 准备工作

通过一个小方块平移运动模拟浏览器渲染图形

 1.block {
 2    position: absolute;
 3    left: 100px;
 4    top: 100px;
 5    width: 50px;
 6    height: 50px;
 7    background: #f00;
 8    animation: move 3s linear infinite alternate;
 9}
10
11@keyframes move {
12    0% {
13        left: 100px;
14    }
15    100% {
16        left: 300px;
17    }
18}

使用while模拟js阻塞

 1function task() {
 2    const now = performance.now()
 3    while(performance.now() - now < 1) {}
 4}
2. 测试正常运行js耗费时间
 1function run1() {
 2    const data = new Array(2000)
 3    console.time("normal")
 4    for(let i = 0; i < data.length; i++) {
 5        task()
 6    }
 7    console.timeEnd("normal")
 8}

可以看到总耗时2s,页面在js运行期间卡住了,小方块完全没有响应,直到js运行结束后才正常播放动画。

3. 测试使用宏任务方案延迟计算
 1function setTimeoutTask() {
 2    return new Promise((s) => {
 3        setTimeout(() => {
 4            task()
 5            s()
 6        }, 0)
 7    }) 
 8}
 9function run2() {
10    const data = new Array(2000)
11    console.time("hong")
12    let list = []
13    for(let i = 0; i < data.length; i++) {
14        list.push(setTimeoutTask())
15    }
16    Promise.all(list).then(() => {
17        console.timeEnd("hong")
18    })
19}

宏任务方案基本原理是利用setTimeout事件会放到下一个宏任务内运行,从而减少了单个渲染帧的计算压力,但还是会出现间断卡顿的现象。

4. 测试使用空闲帧方案延迟计算
 1function idleTask(task) {
 2    return new Promise((s) => {
 3        window.requestIdleCallback((IdleDeadline) => {
 4            let timeRemain = IdleDeadline.timeRemaining();
 5            if(timeRemain > 0) {
 6                task()
 7                s()
 8            } else {
 9                idleTask(task).then(() => {
10                    s()
11                })
12            }
13        })
14    })
15}
16function run3() {
17    const data = new Array(2000)
18    console.time("idle")
19    let list = []
20    for(let i = 0; i < data.length; i++) {
21        list.push(idleTask(task))
22    }
23    Promise.all(list).then(() => {
24        console.timeEnd("idle")
25    })
26}

这里用到requestIdleCallback API,MDN上的说明是“这个函数将在浏览器空闲时期被调用。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应”。个人理解就是在每一帧结束后,如果有空闲时间就会调用。

函数回调内有个值”IdleDeadline.timeRemaining()“用于说明当前帧还剩余多少时间。如果大于0,说明有剩余时间可以用于执行js计算。

实际效果确实是没有卡顿感了,但是相应的执行耗时也比普通执行要长

demo: code.juejin.cn/api/raw/733…

个人笔记记录 2021 ~ 2025