前端进阶都应该了解的知识点 – INP

前言

介绍了交互至下一次绘制(INP)取代了首次输入延迟(FID)作为核心 Web 性能指标。INP 衡量用户与页面元素交互时的顿挫感,考虑了交互的每个部分对性能的影响,包括输入延迟、处理时间和呈现延迟。还指出了如何理解 JavaScript 的主线程执行模型以及如何优化 INP 以提升网页性能。今日文章由 @飘飘翻译分享。

从 2023 年 3 月 12 日起,INP 将取代 “首次输入延迟”(FID),成为核心 Web Vital 指标。

FID 和 INP 在浏览器中衡量的是相同情况:当用户与页面上的元素交互时,感觉有多笨拙?对于 Web 及其用户来说,好消息是 INP 通过考虑交互的每个部分和渲染响应,提供了更好的真实性能表现。

对您来说这也是一个好消息:您为确保 FID 的良好分数而采取的措施,将使您在获得可靠的 INP 分数的道路上迈出坚实的一步。当然,任何数字 — 不管是令人舒心的绿色还是令人担忧的红色 — 在不知道其确切来源的情况下,都不会有任何特别的用处。事实上,理解替换的最佳方法是更好地理解被替换的内容。正如前端性能的许多方面一样,关键在于了解 JavaScript 如何使用主线程。正如您所想象的那样,每个浏览器管理和优化任务的方式都略有不同,因此本文会将简化一些概念,但请不要误会,您对 JavaScript 的事件循环了解得越深入,就越能更好地处理各种前端性能工作。

主线程

您可能在过去听说过将 JavaScript 描述为 “单线程” 的说法,虽然自 Web Worker 出现后,这种说法已不完全正确,但它仍然是描述 JavaScript 同步执行模式的有用方式。在一个给定的 “领域” 内,比如 iframe、浏览器标签页或 Web Worker,一次只能执行一个任务。在浏览器选项卡的上下文中,这种顺序执行被称为主线程,它与其他浏览器任务共享,如解析 HTML、某些 CSS 动画以及渲染和重新渲染页面的某些部分。

JavaScript 使用一种名为 “调用栈”(或简称 “栈”)的数据结构来管理 “执行上下文”– 即当前主线程正在执行的代码。当脚本启动时,JavaScript 解释器会创建一个 “全局上下文” 来执行代码的主体 — 任何存在于 JavaScript 函数之外的代码。全局上下文被推送到调用栈,并在那里执行。

当解释器在全局上下文的执行过程中遇到函数调用时,它会暂停全局执行上下文,为该函数调用创建一个 “函数上下文”(有时也称为 “局部上下文”),并将其推送到堆栈顶部,然后执行该函数。如果该函数调用包含一个函数调用,则会为其创建一个新的函数上下文,将其推到栈顶并立即执行。栈中最高的上下文总是当前正在执行的上下文,当执行结束时,它会从栈中弹出,这样下一个最高的执行上下文就可以继续执行 –“后进先出”。最终,执行将回到全局上下文,要么遇到另一个函数调用,执行将通过该函数调用和调用所包含的任何函数逐次向上和向下执行,要么全局上下文结束,调用栈清空。

如果 “按照遇到的顺序一个一个地执行每个函数” 就是全部内容,那么执行任何异步任务(例如从服务器获取数据或触发事件处理程序的回调函数)的函数都将是性能灾难。该函数的执行上下文要么会阻塞执行,直到异步任务完成,该任务的回调函数启动,要么会突然中断任务完成时调用堆栈中的任何函数上下文。因此,除了堆栈之外,JavaScript 还使用了由 “事件循环” 和 “回调队列”(或 “消息队列”)组成的事件驱动 “并发模型”。

当异步任务完成并调用其回调函数时,该回调函数的函数上下文会被放入回调队列,而不是调用栈的顶部 — 它不会立即接管执行。位于回调队列和调用栈之间的是事件循环,它不断轮询回调队列中是否存在函数执行上下文,以及调用栈中是否有空位。如果回调队列中有函数执行上下文在等待,而事件循环确定调用堆栈是空的,那么该函数执行上下文就会被推送到调用堆栈,并像同步调用一样执行。

例如,我们有一个脚本,使用老式的 setTimeout 在 500 毫秒后向控制台记录日志:

 setTimeout( function myCallback() {
     console.log( "Done." );
 }, 500 );

 // Output: Done.

首先,为脚本正文创建一个全局上下文并执行。全局执行上下文会调用 setTimeout 方法,因此会在调用栈顶部创建 setTimeout 的函数上下文,并执行该函数 — 这样计时器就开始滴答作响了。然而,myCallback 函数并没有被添加到堆栈中,因为它还没有被调用。由于 setTimeout 没有其他事情可做,它被从堆栈中弹出,全局执行上下文恢复。在全局上下文中没有其他事情可做,所以它从堆栈中弹出,而堆栈现在是空的。

现在,在这一系列事件中的任何时候,我们的定时器都会过期,从而调用 myCallback。此时,回调函数将被添加到回调队列中,而不是被添加到堆栈中并中断其他正在执行的操作。一旦调用栈为空,事件循环就会将 myCallback 的执行上下文推送到栈中执行。在这种情况下,主线程早在定时器结束前就完成了工作,而我们的回调函数会立即添加到空的调用栈中:

 const rightNow = performance.now();

 setTimeout( () => {
     console.log( `The callback function was executed after ${ performance.now() - rightNow } milliseconds.` );
 }, 500);

 // Output: The callback function was executed after 501.7000000476837 milliseconds.

在主线程没有其他事情可做的情况下,我们的回调会准时触发,大约需要一两毫秒。但是,一个复杂的 JavaScript 应用程序在全局执行上下文结束之前,可能有数以万计的函数上下文需要执行,而浏览器的速度再快,这些事情也需要时间。因此,让全局执行上下文通过一个 while 循环保持忙碌,并快速计数到 5 亿 — 这是一项漫长的任务,从而伪造出一个拥挤不堪的主线程。

 const rightNow = performance.now();
 let i = 0;

 setTimeout( function myCallback() {
   console.log( `The callback function was executed after ${ performance.now() - rightNow } milliseconds.`);
 }, 500);

 while( i < 500000000 ) {
   i++;
 }
 // Output: The callback function was executed after 1119.5999999996275 milliseconds.

全局执行上下文再次创建并执行。几行代码后,它调用了 setTimeout 方法,因此在调用栈顶部创建了 setTimeout 的函数执行上下文,计时器开始滴答作响。setTimeout 的执行上下文完成并从堆栈中弹出,全局执行上下文恢复,我们的 while 循环开始计数。

与此同时,500 毫秒计时器计时结束,myCallback 被添加到回调队列中,但这次调用栈并不是空的,事件循环必须等待全局执行上下文的剩余时间,才能将 myCallback 移到栈中。与处理整个客户端渲染的网页所需的复杂处理相比,对于在现代笔记本电脑上运行的现代浏览器来说,”数到一个相当大的数字” 并不是最繁重的工作,但我们仍然可以看到结果上的巨大差异:在我的例子中,输出显示所需的时间是预期时间的两倍多。

现在,我们使用 setTimeout 是出于可预测性的考虑,但事件处理程序的工作方式也是一样的:当 JavaScript 解释器在全局或函数上下文中遇到事件处理程序时,事件就会被绑定,但与该事件监听器相关的回调函数不会被添加到调用堆栈中,因为该回调函数尚未被调用 — 直到事件触发为止。一旦事件触发,该回调函数就会添加到回调队列中,就像我们的计时器耗尽一样。那么,如果一个事件回调开始,比如说,当主线程被埋在长任务中时,会发生什么?为了让一个 JavaScript 密集的页面启动并运行,需要进行数兆字节的函数调用。

如果用户立即点击了这个 button 元素,回调函数的执行上下文就会创建并添加到回调队列中,但在堆栈中有足够空间之前,它无法被移动到堆栈中。从纸面上看,几百毫秒似乎并不算什么,但用户交互与交互结果之间的任何延迟都会对感知性能造成巨大影响 — 问问小时候玩过任天堂的人就知道了。这就是 “首次输入延迟”:在主线程闲置的情况下,对用户触发事件处理程序的第一个点与调用事件处理程序回调函数的第一个机会之间的延迟进行测量。一个页面在解析和执行大量 JavaScript 以获得渲染和功能时会陷入困境,因此在调用堆栈中没有空间让事件处理程序的回调函数立即排队,这意味着用户交互与回调函数被调用之间的延迟会更长,页面也会感觉缓慢、滞后。

这就是 “首次输入延迟”– 一个非常重要的指标,但它并不能反映用户体验页面的全部情况。

什么是 INP?

毫无疑问,事件与事件处理程序的回调函数执行之间的长时间延迟是不好的,但在现实世界中,”回调函数的执行上下文被移动到调用堆栈的机会” 并不是用户点击按钮时想要的结果。真正重要的是交互与交互的可见结果之间的延迟。

这就是 “下一次绘制的交互” 所要测量的内容:用户交互与浏览器下一次绘制之间的延迟,即向用户提供交互结果可视化反馈的最早机会。在用户访问页面期间测量的所有交互中,交互延迟最差的交互将作为 INP 分数显示,毕竟在追踪和修复性能问题时,我们最好先处理坏消息。

总而言之,交互有三个部分,所有这些部分都会影响页面的 INP:输入延迟、处理时间和呈现延迟。

图片

输入延迟

事件处理程序的回调函数从回调队列到主线程需要多长时间?

现在你对这个问题已经了如指掌 — 它与 FID 曾经捕捉到的指标是一样的。不过,INP 比 FID 更进一步:FID 仅基于用户的第一次交互,而 INP 则考虑了用户在页面上的所有交互,以便更准确地反映页面的总体响应速度。INP 跟踪硬件或屏幕键盘上的任何点击、敲击和按键操作 — 这些互动最有可能促使页面发生可见的变化。

处理时间

与事件相关的回调函数运行需要多长时间?

即使事件处理程序的回调函数立即启动,该回调函数也会调用更多的函数,从而填满调用堆栈,并与主线程上的其他工作竞争。

 const myButton = document.querySelector( "button" );
 const rightNow = performance.now();

 myButton.addEventListener( "click", () => {
     let i = 0;
     console.log( `The button was clicked ${ performance.now() - rightNow } milliseconds after the page loaded.` );
     while( i < 500000000 ) {
         i++;
     }
     console.log( `The callback function was completed ${ performance.now() - rightNow } milliseconds after the page loaded.` );
 });

 // Output: The button was clicked 615.2000000001863 milliseconds after the page loaded.
 // Output: The callback function was completed 927.1000000000931 milliseconds after the page loaded.

假设主线程中没有其他阻塞并妨碍该事件处理程序的回调函数,那么该点击处理程序的 FID 分数就会很高,但回调函数本身包含一个庞大而缓慢的任务,可能需要很长时间才能运行并向用户显示结果。缓慢的用户体验,用一个欢快的绿色结果来概括是不准确的。

与 FID 不同,INP 将这些延迟也考虑在内。用户交互会触发多个事件 — 例如,键盘交互会触发按下、按上和按下事件。对于任何给定的交互,INP 都会捕捉 “交互延迟” 最长的事件的结果,即用户交互与渲染响应之间的延迟。

演示延迟

主线程进行渲染和合成工作的速度如何?

请记住,主线程不仅要处理 JavaScript,还要处理渲染。处理事件处理程序创建的所有任务所花费的时间现在都在与主线程的其他进程竞争,所有这些进程现在都在与绘制结果所需的布局和样式计算竞争。

测试与 INP 的相互作用

现在,您已经对 INP 的测量方法有了更好的了解,是时候开始在现场收集数据和在实验室进行修补了。

对于 Chrome 浏览器用户体验报告数据集中包含的任何网站,PageSpeed Insights 都是开始了解网页 INP 的好地方。要从各种不可知的连接速度、设备能力和用户行为中收集真实世界的数据,Chrome 浏览器团队的网络生命周期 JavaScript 库(或专注于性能的第三方用户监控服务)可能是最好的选择。

然后,一旦您从现场测试中了解到您的网页最大的 INP 问题,Web Vitals Chrome 扩展就可以让您在浏览器中对交互进行测试、修补和重新测试 — 虽然不如现场数据那样具有代表性,但对于处理现场测试中出现的任何棘手的时间问题却至关重要。

优化 INP

现在,您已经对 INP 在幕后的工作原理有了更好的了解,并能找出网页中最大的 INP 问题所在,是时候开始整顿了。从理论上讲,INP 的优化非常简单:去掉那些冗长的任务,避免复杂的布局重新计算让浏览器不堪重负。

遗憾的是,简单的概念在实践中并不能转化为快速、简单的技巧。就像大多数前端性能工作一样,优化 “Next Paint” 也是一个 “寸进” 的游戏 — 测试、修补、重新测试,逐步将页面调整到更小、更快、更尊重用户时间和耐心的程度。

原创文章,作者:guozi,如若转载,请注明出处:https://www.sudun.com/ask/79556.html

Like (0)
guozi的头像guozi
Previous 2024年5月30日 下午6:57
Next 2024年5月30日 下午6:58

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注