宇宙厂:为什么前端要了解InteractiontoNextPa...

前有科技后进阶 2025-04-26 14:18:31

大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!

2024 年 3 月,Interaction to Next Paint (INP) 成为 Google Core Web Vitals 的一部分,这是一组基于现场数据衡量网页用户体验的指标,用于 Google 的搜索排名。

1. 了解令人困惑的指标 INP

INP 是迄今为止所有 Core Web Vitals 指标中最容易被误解的。

INP 常常被误解为需要在 200 毫秒内绘制完整的用户响应,而它实际上是为了向用户反馈输入正在那段时间内被处理。

下面是用 JSX 渲染的网页并具有优秀的 INP 分数:

<button onClick={() => {}}>Click me</button>

显然,空的事件处理函数执行很快,即开发者不必实际更改 DOM 即可将其算作 INP。这里的 “绘制 (Paint)” 实际上是意味着浏览器有机会进行绘制,页面是否发生变化对该指标来说并不重要。例如,下面的代码示例阻塞主线程 1 秒:

<button onClick={async () => { await sleep(1000); blockTheMainThreadForOneSecond(); }}> Click me</button>

此时,如果单击按钮也会获得较好的 INP 分数,因为 sleep(1000) 为浏览器提供了绘制时间,此时主线程是否阻塞则关系不大。这一点和下面经常编写的代码非常类似:

<button onClick={async () => { const data = await fetchData(); blockTheMainThreadForOneSecond(data); }}> Click me</button>

然而,下面示例代码对 INP 却非常不友好:

<html> <button onClick={() => { blockTheMainThreadForOneSecond(); }} > Click me </button></html>

为了优化 INP 分数,开发者可以做的是把任务调度尽快交给浏览器以便绘制并对用户操作做出反应。但是,这并不意味着需要一次性执行完所有工作,而只需要将单次任务的时间控制在 200 毫秒的 INP 截止期限内即可。

网上很多地方说有 GUI 线程和 JS 引擎线程而且互斥,实际上是因为 Paint 任务没有及时执行,即绘制列表 (Display List) 没有及时提交给合成器线程,` 而根源是 JS 执行与「渲染相关任务」都在争夺主线程有限的资源 `。

2. INP 指标数据的时间构成

对于浏览器来说,从用户交互到下一次绘制的时间可以拆分为以下时段:

用户开始交互事件处理程序可能被恰好在主线程上运行的其他代码阻塞事件处理程序运行不相关的代码还有另一个机会阻止主线程如果其他代码更改了 DOM,浏览器将重新渲染 DOM最终绘制(Paint)

为了优化 INP,开发者需要尽量缩短上述所有阶段的持续时间,核心包括:

优化 INP 阶段的三方代码:在 Web 模型中,运行的所有代码默认 共享单个线程和事件循环,这意味着不相关的代码可能会干扰用户事件执行。这最难优化,例如:第三方 JS 导致页面的 INP 受影响。INP 阶段的事件处理 :完全可控、用于对用户交互做出反应,例如:获取数据、更改 DOM 等,最终影响 INP 分数。在现代 Web 应用中,该阶段的主要成本是重新渲染,包括状态更改时的虚拟 DOM 对比。对于 React,经典的性能优化(如缓存和不可变上下文和 prop 值)是最小化此阶段持续时间的最佳机制。INP 阶段的浏览器渲染 :在此阶段,浏览器会获取上一阶段的所有更改,并将其转换为可以绘制的实际像素。虽然浏览器经常要求 Web 开发人员将 “任务分块”,但浏览器本身仍然以整体方式执行渲染。优化此阶段的要点是,大多数浏览器布局算法都是 O(n),开发者的工作是尽量减少失效的 DOM 数量。3. 如何提升 INP 的几种方案3.1 将操作和响应分开的 interactionResponse

在低端设备上,渲染速度很容易多于 200 毫秒。那么,在这种情况下开发者能做什么? INP 的本意是希望在 200 毫秒内响应用户输入而非在 200 毫秒内实际绘制响应。

因此,开发者此时可以做的是将交互响应分为两个阶段,即 确认用户交互 和 实际更改页面,比如下面的 LanguagePicker 组件示例:

import { useState } from "react";export function LanguagePicker({ setLanguage }) { const [selected, setSelected] = useState(); return ( <select Name={selected ? `value-${selected}` : ""} onChange={(e) => { setSelected(e.target.value); setLanguage(e.target.value); // 同时执行可能非常耗时 }} > <option value="JS">JavaScript</option> <option value="TS">TypeScript</option> </select> );}

当用户选择新值时,会向 select 元素添加一个 并设置新语言。但是设置新语言的操作可能非常耗时。此时开发者可以做的是将响应用户操作和设置新语言分开,以便用户立即知道操作正在被处理,而不必等待昂贵的工作完成。

import { useState } from "react";export function LanguagePicker({ setLanguage }) { const [selected, setSelected] = useState(); return ( <select Name={selected ? `value-${selected}` : ""} onChange={async (e) => { setSelected(e.target.value); await interactionResponse(); // 让浏览器去绘制 setLanguage(e.target.value); }} > <option value="JS">JavaScript</option> <option value="TS">TypeScript</option> </select> );}

以上代码可以确保立即确认用户操作并且损失最少的 INP 分数,接着再执行昂贵的操作。事实上,因为使用了浏览器原生的 select,所以不需要做任何事情来确认用户操作:

export function LanguagePicker({ setLanguage }) { return ( <select onChange={async (e) => { await interactionResponse(); // 先让浏览器去绘制 setLanguage(e.target.value); // 执行耗时操作 }} > <option value="JS">JavaScript</option> <option value="TS">TypeScript</option> </select> );}

以上 interactionResponse 的方法实现起来也非常简单:

export function interactionResponse(): Promise<unknown> { return new Promise((resolve) => { setTimeout(resolve, 100); // 防止 rAF 未触发,例如:移动到别的选项卡 requestAnimationFrame(() => { setTimeout(resolve, 0); }); });}

通过 interactionResponse 方法可以让浏览器立即绘制帧,接着运行业务代码。通过延迟代码执行乍一看似乎很讽刺,但需要注意该方法有以下好处:

立即向用户反馈操作已被接受最多只延迟 1 帧,平均 8 毫秒,对人眼来说不明显

当然,开发者还可以结合以下方式优化 INP:

通过内联 SVG 来减少 DOM 数量CSS 动画使用 opacity 来减少回流,同时可以直接在 GPU 中合成虚拟化滚动、虚拟化 DOM 等3.2 使用 React 的 startTransition

React 引入了一个非常相似的 API,称为 startTransition,其告诉 React 其回调中的 状态更新 不需要同步执行。如果应用中的操作缓慢是由于状态更改造成的(如上例所示),那么开发者只需添加 startTransition 即可。

import { startTransition } from "react";export function LanguagePicker({ setLanguage }) { return ( <select onChange={(e) => { startTransition(() => { setLanguage(e.target.value); }); }} > <option value="JS">JavaScript</option> <option value="TS">TypeScript</option> </select> );}参考资料

声明:该文章部分内容来自 Vercel 的 CTO Malte Ubl 的《Demystifying INP: New tools and actionable insights》,但是很多地方结合自己的理解做了修改。

https://vercel.com/blog/demystifying-inp-new-tools-and-actionable-insights

https://vercel.com/docs/speed-insights

https://zhuanlan.zhihu.com/p/501608119

https://www.ezrankings.com/blog/interaction-to-next-paint-inp/

0 阅读:0

前有科技后进阶

简介:感谢大家的关注