应用 Performance 专用工具深刻理解事件循环

lxf2023-03-17 13:25:01

看过开光大大的 @zxg_神说要有光 的帖子 Admin.net/post/715535… ,应用 Performance 专用工具深刻理解事件循环。这真是一个好方法,看 spec 看大半天比不上看一眼 performance 的 trace!但那么问题来了,我明白这玩意儿就是这个次序,面试问题也背过一堆,但有什么作用?

举例说明

大家有这样一段编码

  const sleep = (ms) => {
    let s = Date.now();
    while (Date.now() - s < ms) {
    }
  };
  const doit = () => {
    sleep(1000);
    Promise.resolve().then(() => {
      sleep(1000);
    });
    setTimeout(() => {
      sleep(1000);
    });
    requestAnimationFrame(() => {
      sleep(1000);
    });
  };

应用 performance 专用工具查询运作全过程,他是这样子:

应用 Performance 专用工具深刻理解事件循环

ok,那么我们逐渐

微任务

新创建一个微任务非常简单:

Promise.resolve().then(() => {
    // 微任务在这里运作
});

由图中可以看到,“当前任务”运作完毕,紧接着微任务便会实行。

应用 Performance 专用工具深刻理解事件循环

那可以做什么用呢?

由此可见/可互动用时统计分析。 lighthouse 只有给你一个大约的参考时长,没法拿来做平稳线上指标值跟踪。比如你要统计分析 TTI,那样数据信息回来以后, React 3D渲染完毕,这时就能称之为 TTI。那样用微任务就非常好,正好是这个时间段,例如:

function main() {
    const [ data, setData ] = useState([]);
    useEffect(() => {
        request().then((data) => {
            setData(data);
            Promise.resolve().then(() => {
                reportTTI(performance.now());
            });
        })
    });
    return (
        <ul>
            {...data.map(e => <li>{e}</li>}
        </ul>
    );
}

不好的消息是,那样 hack 了 react 的实现,这默认设置 setData 不被分得好几个任务时实行。但 react 不容易每天更新,那样大致或是够用的。

屎山编码 bug 修补: 不会再维修的屎山代码的 bug 修补,时序逻辑繁杂得了了分,我们需要挽救自己工作。

宏任务

// 宏任务在这里运作
function nextTick(callback) {
    setTimeout(() => {
        callback();
    });
}

// 另外一种新创建方法
// 比 setTimeout 好,由于递归函数 setTimeout 会间距变长。 
function nextTick(callback) {
    const { port1, port2 } = new MessageChannel();
    port1.onmessage = callback;
    port2.postMessage();
}

Note: 仅为了能演试,省略了一堆小细节。

这一非常常见,很多人都会使用这种做每日任务分块。例如 react-scheduler,交出主进度的时长,将 cpu 交给制作,例如这篇文章:zhuanlan.zhihu.com/p/570318956

这儿介绍一个更有趣游戏的玩法,提升首屏。

举一个情景,你刚接任一个项目,发觉首屏比较慢,你就会觉得埋点报送的用时较长,举例说明:

function firstScreen() {
    task1();
    collectData();
    task2();
    collectData();
    task3();
}

大家看一下 performance

应用 Performance 专用工具深刻理解事件循环

哇,首屏一共 2.5s,2个埋点汇报就需要 100ms * 2,这怎么忍?

好消息是,资询了一波,很多人都认为埋点的实用性并不重要,那我们有方法让 collectData 没有在首屏实行吗?大家改下编码:

function firstScreen() {
    task1();
    nextTick(collectData);
    task2();
    nextTick(collectData);
    task3();
}

换句话说,大家让 collectData 在下一个宏任务内实行。 再看一下 performance。

提升前:

应用 Performance 专用工具深刻理解事件循环

提升后:

应用 Performance 专用工具深刻理解事件循环

哈,collectData 被成功移到了首屏后!所以只要 10 min解决。

一种特殊的生产调度方法 requestAnimationFrame

我觉得不是很强烈推荐这个方法生产调度,由于 requestAnimationFrame 有一个致命的缺点 --- 假如网页页面看不到,requestAnimationFrame 的调用函数都不会实行。 spec 是这样说的:paint 以前实行 requestAnimationFrame,这也意味着 requestAnimationFrame 何时开启就看电脑浏览器完成。

如果你需要编辑 requestAnimationFrame 和宏任务的时钟频率,这就意味着恶梦:

举一个 bad case:

function howLoopWork(ms) {
    const s = Date.now();
    requestAnimationFrame(() => {
        console.log(2);
    });
    while(Date.now() - s < ms) {};
    setTimeout(() => {
        console.log(1);
    });
}

howLoopWork(10); // log: 1 2
// 等一会再执行
howLoopWork(100); // log: 2 1

别以为这就没有了?大家就变成个部位:

function howLoopWork(ms) {
    const s = Date.now();
    while(Date.now() - s < ms) {};
    setTimeout(() => {
        console.log(1);
    });
    requestAnimationFrame(() => {
        console.log(2);
    });
}

howLoopWork(10); // log: 1 2
// 等一会再执行
howLoopWork(100); // log: 1 2 

那 requestAnimationFrame 现在除了动画和制作前改 DOM 就没卵用了没有?

咱们就举几个 requestAnimationFrame 神秘主要表现:

function howLoopWork() {
    requestAnimationFrame(() => console.log(1));
    requestAnimationFrame(() => console.log(2));
    setTimeout(() => {
        const s = Date.now();
        console.log(3);
        while(Date.now() - s < 10) {};
    });
}
howLoopWork(); // 1 2 3

此次祝贺你了,结论一定是 1 2,在一个任务内实行好几个 requestAnimationFrame,那样他会在盈利 paint 时按顺序开启。

应用 Performance 专用工具深刻理解事件循环

所以再改一下:

function howLoopWork() {
    requestAnimationFrame(() => {
        console.log(1);
        requestAnimationFrame(() => console.log(2)); // 第 4 行
    });
    setTimeout(() => {
        const s = Date.now();
        while(Date.now() - s < 10) {};
    });
}
howLoopWork(); // 1 3 2

嘿嘿,此次有点儿不一样,怎么回事?

和微每日任务不一样,在 callback 内嵌套调用 requestAnimationFrame,其实就是第 4 行,嵌入的 callback 会再下一次 paint 实行! 这样说有点儿抽象化,我们来看一下 performance:

应用 Performance 专用工具深刻理解事件循环

行吧,了解了又如何。能够监管首屏啊。

我们都知道一次每日任务的操作流程应该是:

当前任务实行 -> requestAnimationFrame -> paint

那样不论是微任务或是 requestAnimationFrame 都难以监管到 paint 时期的用时,这样说嵌入 requestAnimationFrame 就能,但问题也非常明显:

  • 网页页面看不到时远走高飞,间距会变得十分长
  • 正中间宏任务插进过多也凉,下一次 paint 被推迟很多

大家实践活动了一段时间后直接放弃了这种行为,老老实实应用微任务的形式,因为人的 paint 用时非常短。

这一章节目录就全当给大家介绍了一个有意思的技术性手机游戏。

再去一道面试问题

function howLoopWork() {
    let a = 0;

    setTimeout(() => {
        a = 1;
        setTimeout(() => {
            console.log('4', a); 
        });
    });
    Promise.resolve().then(() => {
        console.log('1', a);
        Promise.resolve().then(() => {
            const s = Date.now();
            while(Date.now() - s < 16) {};
            console.log('2', a);
        });
    });
    requestAnimationFrame(() => {
        console.log('3', a);
        requestAnimationFrame(() => {
            console.log('5', a);
        });
    });
    console.log('0', a);
}

howLoopWork();

回答:

应用 Performance 专用工具深刻理解事件循环

你理解了吗~

Note:之上都为我日常工作汇报,热烈欢迎加我好友,一起玩啊

应用 Performance 专用工具深刻理解事件循环