为什么 Interaction to Next Paint 改变了开发者对响应性的思考
Source: Dev.to
介绍
多年来,First Input Delay (FID) 是 Core Web Vitals 中衡量响应性的指标。它只测量一件事:用户首次交互与浏览器开始处理该交互之间的延迟。大多数站点都能轻松通过 FID,因为它只捕获第一次点击的输入延迟,忽略其后的所有交互。即使页面在打开下拉菜单时卡顿两秒,FID 也不会记录下来。
2024 年 3 月,Google 用 Interaction to Next Paint (INP) 替代了 FID。此次更换并非表面文章。INP 从根本上改变了网页应用“响应”的定义,它通过测量整个页面访问期间的每一次交互——而不仅仅是第一次交互——并通过 输入延迟、处理以及绘制 三个阶段追踪每一次交互的完整生命周期。
本文将说明这一变化为何重要、INP 实际测量了什么,以及在规则改变后我们该如何看待响应性。
为什么 FID 不足
- 仅测量第一次交互。如果你的页面加载时表现良好,但在 JavaScript 水合后、状态更新触发重渲染后,或第三方脚本加载后变得迟缓,FID 仍会给出良好分数。用户在慢速、卡顿的界面上的体验不会体现在该指标中。
- 仅计入输入延迟。FID 忽略了事件处理函数本身的执行时长以及浏览器在处理函数完成后更新屏幕所需的时间。一次输入延迟为零但处理和绘制耗时 800 ms 的点击,在 FID 下也会得到完美分数。
- 通过率过高——Chrome 对真实用户数据的分析显示,超过 90 % 的网站能够通过 100 ms 的 FID 阈值,这使得该指标几乎失去了区分度,而用户仍然感受到网站慢且不响应。
Photo by cottonbro studio on Pexels
Interaction to Next Paint (INP)
INP 捕获一次交互的完整生命周期,分为三个阶段:
| 阶段 | 描述 |
|---|---|
| 输入延迟 | 用户操作(点击、轻触、按键)与浏览器开始运行你的事件处理函数之间的时间。这是 FID 所测量的内容,但 INP 会对 每一次 交互进行捕获,而不仅仅是第一次。 |
| 处理时间 | 你的事件处理函数执行所需的时长。如果点击按钮会触发状态更新,导致 React 重新渲染一个大型组件树,则整个渲染过程都算作处理时间。 |
| 呈现延迟 | 处理函数完成后,浏览器在屏幕上更新像素的时间。此过程包括布局计算、绘制和合成。修改会触发布局的 CSS 属性(例如 width、height、position)会使此延迟显著增加。 |
页面访问的 INP 值通常是延迟最差的那次交互(对交互次数很多的页面会进行统计调整)。
Google 的阈值
| INP 分数 | 含义 |
|---|---|
| ≤ 200 ms | 优秀 |
| 200 – 500 ms | 需要改进 |
| > 500 ms | 差 |
web.dev INP 文档 解释了统计方法。对大多数站点而言,报告的 INP 值是 第 98 百分位的交互,这意味着它反映了普通用户在访问期间可能遇到的最差体验。
从“快速加载”到“快速使用”
在 FID 下
- 响应性主要是避免在初始页面加载期间出现长任务。
- 只要 JavaScript 在第一次点击之前没有阻塞主线程,就算合格。
- 优化重点放在代码拆分、懒加载以及推迟非关键脚本——本质上是“快速加载”的策略。
在 INP 下
- 响应性必须在整个会话期间保持。
- 关注点转向“快速使用”。
通过 FID 但在 INP 中失败的模式
- 交互时的重状态更新 – 点击一个会重新渲染 500 条列表项的过滤器会阻塞主线程。 在 FID 中,这种情况除非恰好是第一次交互,否则是不可见的;在 INP 中,它每次都会被计入。
- 事件处理器中的同步布局计算 – 在点击处理函数中读取布局属性(
offsetHeight、getBoundingClientRect())会强制布局,阻塞主线程。详情请参阅 Google 关于强制布局的文档。 - 交互时运行的第三方脚本 – 每次点击都会触发的分析脚本、首次交互时初始化的聊天小部件,以及拦截触摸的同意管理平台,都会对 INP 产生影响。
“INP 把讨论从‘页面加载有多快’转变为‘页面对用户每一次操作的响应速度有多快’。这是一道更高的门槛,也正是用户真正关心的。” – Dennis Traina,137Foundry
Source: …
实用 INP 优化
1. 拆分冗长的事件处理程序
如果点击处理函数触发的工作耗时 > 50 ms,请将其拆分。使用 scheduler.yield()(在 Chrome 中可用,其他浏览器需开启标志)或 setTimeout(0) 在工作块之间让出控制权给浏览器。这样可以让浏览器在你的工作块之间处理待处理的用户输入并进行绘制更新。
async function handleFilterClick(selectedFilter) {
// 立即更新 UI
showLoadingIndicator();
// 让出控制权,让浏览器绘制加载状态
await scheduler.yield();
// 现在执行耗时的过滤操作
const filtered = applyFilter(data, selectedFilter);
// 再次让出控制权再渲染结果
await scheduler.yield();
renderResults(filtered);
}2. 防抖快速交互
滚动处理器、窗口大小变化监听以及即时搜索输入框可能每秒触发数十次事件。对它们进行防抖,使得昂贵的工作在用户停止交互后 只运行一次,而不是在每个单独的事件上都执行。
function debounce(fn, wait) {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => fn.apply(this, args), wait);
};
}
// 示例:对搜索输入进行防抖
const handleSearch = debounce((query) => {
fetchResults(query).then(renderResults);
}, 300);3. 将计算移至 Web Worker
数据处理、排序或任何不需要直接访问 DOM 的 CPU 密集型工作应当卸载到 Web Worker。这可以让主线程保持空闲,以便处理用户交互和绘制。
// main.js
const worker = new Worker('worker.js');
worker.postMessage({ type: 'sort', payload: largeArray });
worker.onmessage = (event) => {
const sorted = event.data;
renderSortedList(sorted);
};// worker.js
self.onmessage = (event) => {
if (event.data.type === 'sort') {
const sorted = event.data.payload.sort((a, b) => a - b);
self.postMessage(sorted);
}
};4. 减少强制同步布局
- 避免在同一任务中修改 DOM 后立即读取布局属性(
offsetHeight、clientWidth等)。 - 将 DOM 读取和写入分批进行,或使用
requestAnimationFrame模式。
// 糟糕:写后读(强制布局)
button.addEventListener('click', () => {
button.style.width = '200px';
const height = button.offsetHeight; // 强制布局
console.log(height);
});
// 良好:分离读写
button.addEventListener('click', () => {
requestAnimationFrame(() => {
button.style.width = '200px';
});
requestAnimationFrame(() => {
const height = button.offsetHeight;
console.log(height);
});
});5. 审计第三方脚本
- 将分析工具、聊天小部件和同意管理器 按需加载(例如在用户交互后)。
- 使用
async/defer属性或动态import(),防止它们在交互期间阻塞主线程。
测量和监控 INP
- Chrome DevTools – 打开 Performance 面板,记录用户流程,并查找 Interaction to Next Paint 部分。
- Web Vitals JavaScript library – 将
web-vitals添加到站点,以实时捕获 INP:
import { getINP } from 'web-vitals';
getINP((metric) => {
console.log('INP:', metric.value);
// Send to your analytics endpoint
});- Search Console – Google Search Console 现在在 Core Web Vitals 报告中显示 INP,展示良好 / 需要改进 / 差的页面分布。
结论
INP 用来取代 FID,提供对整个用户旅程中响应性的整体视图。通过测量每一次交互的输入延迟、处理时间和呈现延迟,它能够揭示之前未被注意到的性能问题。
好消息是,同样的性能工程原则——短任务、异步工作、避免强制布局以及谨慎的第三方管理——仍然适用,只是现在必须在页面生命周期的整个过程中持续应用它们。
首先审计最耗时的交互,拆分繁重的处理程序,并利用 Web Workers 和防抖技术。采用这些模式后,你的网站将在 INP 指标上从“需要改进”提升到“良好”,更重要的是,为真实用户提供更流畅、更快速响应的体验。
Web Workers
大型数组和解析 JSON 负载 不需要 在主线程上执行。Web Worker 在后台线程中运行 JavaScript,无法阻塞用户交互。
优化 React 重渲染
React.memo– 防止函数组件进行不必要的重新渲染。useMemo– 对昂贵的计算进行记忆,仅在其依赖项变化时才执行。useTransition– 将非紧急的状态更新标记为低优先级,在大量更新期间保持 UI 响应。
React 文档中关于 Transitions 的章节解释了
startTransition如何在大量更新期间保持 UI 响应。
核心网页指标概览
想要更全面地了解所有三个核心网页指标以及诊断工作流程——从现场数据到修复,请参阅 核心网页指标完整指南,该指南涵盖 LCP、CLS 和 INP。
照片作者:Daniil Komov,来源于 Pexels
实时用户监控 (RUM)
实验室工具如 Lighthouse 能模拟交互,但它们无法复制真实用户行为的多样性。有些用户点击非常迅速,有些使用键盘导航,还有些会与您的测试脚本从未触及的元素进行交互。
实时用户监控是跟踪 INP 的唯一可靠方式。
使用 web‑vitals 库进行 INP 监控
web‑vitals库会报告来自真实访客的 INP 值。- 将其集成到你的分析流水线中,以查看哪些页面和哪些交互的 INP 值最高。
- 归因构建能够识别导致最差交互的确切元素和事件类型。
137Foundry 的团队通常将 web‑vitals 监控与 Chrome DevTools → Interactions 轨道结合使用,以诊断现场数据中识别出的特定慢速交互。这种实时用户检测与实验室调查相结合的方式是解决 INP 问题的最快路径。
INP 标准
INP 将响应式网页应用的标准提升到了新的高度。
- FID 只测量单个时刻,相对容易达标。
- INP 要求 每一次交互、每一个页面、整个访问期间,都在 200 ms 内作出响应。
这一更高的标准更真实地反映了用户体验。投入 INP 优化的站点实际使用更快,这会转化为:
- 更高的参与度
- 更低的跳出率
- 在竞争激烈的搜索结果中获得排名优势