INP为什么是Core Web Vitals里最难的一关
2024年3月,Google正式用INP(Interaction to Next Paint)替换了FID(First Input Delay),成为Core Web Vitals三大指标之一。和LCP、CLS相比,INP是最难优化的。
原因很简单。根据web.dev官方文档,Chrome使用数据显示用户90%的时间花在页面加载完成之后的交互上。INP测量的就是页面整个生命周期中所有交互的响应速度。你的页面可能有100次交互,其中99次很快,但只要有一次卡了,INP就不合格。
INP的评分标准是:200毫秒以内为Good,200-500毫秒为Needs Improvement,超过500毫秒为Poor。注意这是75百分位,意味着100个用户里有25个用户体验更差。web.dev官方建议把目标定在200毫秒以内。
INP和FID到底有什么区别
FID只测量第一次交互的延迟,而且是测量事件开始到主线程开始处理的时间。这意味着如果你的页面第一次点击很快,但后续操作卡顿,FID是发现不了的。
INP不一样。它测量的是所有点击、点击、键盘交互的完整延迟,从用户操作到浏览器下一次绘制画面为止。这包括三个阶段:输入延迟(事件处理开始前)、处理时间(事件处理执行)、展示延迟(处理完成到画面更新)。
简单说,FID只看”用户点了之后多久开始处理”,INP看的是”用户点了之后多久能看到结果”。INP是用户真实感知到的响应速度。
三个导致INP不合格的常见原因
原因一:主线程被长任务阻塞
JavaScript是单线程的。当一个超过50毫秒的任务在主线程上执行时,用户的所有交互都会被阻塞。这叫”长任务”(Long Task)。
最常见的长任务来源:大量的JavaScript解析和执行、复杂的DOM操作、同步的数据处理。一个典型的例子是页面加载后立即执行一个大型框架的初始化,或者一次性渲染大量列表项。
原因二:第三方脚本拖后腿
第三方脚本是INP的隐形杀手。分析工具、广告SDK、聊天插件、A/B测试工具,每个都可能往主线程上塞任务。一个页面加载5个第三方脚本,每个贡献50毫秒的执行时间,加起来就是250毫秒的阻塞。
原因三:事件处理器太重
有些交互本身就很重。比如点击一个按钮触发大量DOM更新、同步API调用后立即渲染、复杂表单的实时验证。这些操作如果不在事件处理器里做优化,INP必然超标。
五种有效的INP优化方案
方案一:拆分长任务
把一个50毫秒以上的长任务拆成多个小任务,用setTimeout或scheduler.yield()(2024年新API)让出主线程。
// 拆分前:一个200ms的长任务
function heavyWork() {
processChunk1(); // 70ms
processChunk2(); // 70ms
processChunk3(); // 70ms
}
// 拆分后:每个chunk之间让出主线程
function heavyWorkOptimized() {
processChunk1();
setTimeout(() => {
processChunk2();
setTimeout(() => {
processChunk3();
}, 0);
}, 0);
}
// 更好的方式:用scheduler.yield()
async function heavyWorkModern() {
processChunk1();
await scheduler.yield();
processChunk2();
await scheduler.yield();
processChunk3();
}
scheduler.yield()比setTimeout更适合任务拆分,因为它会在当前任务结束后立即恢复执行,而不是排在队列末尾。目前Chrome 129+已支持。
方案二:延迟加载非关键JavaScript
不是所有JavaScript都需要在页面加载时立即执行。用requestIdleCallback把非紧急任务推迟到浏览器空闲时:
// 第三方脚本延迟加载
function loadThirdParty() {
const scripts = [
'analytics.js',
'chat-widget.js'
];
scripts.forEach(src => {
const s = document.createElement('script');
s.src = src;
s.async = true;
document.head.appendChild(s);
});
}
// 等浏览器空闲再加载
if ('requestIdleCallback' in window) {
requestIdleCallback(loadThirdParty, { timeout: 3000 });
} else {
setTimeout(loadThirdParty, 2000);
}
方案三:优化事件处理器
事件处理器里别做重活。如果必须做,用requestAnimationFrame确保在下一帧渲染前完成视觉更新:
button.addEventListener('click', (e) => {
// 立即给用户视觉反馈
button.classList.add('loading');
// 把重操作放到下一帧
requestAnimationFrame(() => {
doExpensiveWork();
button.classList.remove('loading');
});
});
方案四:减少第三方脚本影响
审计你的第三方脚本。每个脚本问三个问题:必须吗?能延迟加载吗?能自己实现替代吗?
对于必须保留的第三方脚本,用Partytown这类工具把它移到Web Worker里执行,避免阻塞主线程。Shopify和Cloudflare都在用这个方案。
方案五:使用Web Worker处理计算密集任务
如果页面有复杂的数据处理(排序、过滤、搜索),把这些逻辑移到Web Worker里。主线程只负责UI更新,Worker负责计算,两边通过postMessage通信。
怎么测量INP
测量INP分现场数据和实验室数据两种。
现场数据用Chrome User Experience Report(CrUX),通过PageSpeed Insights可以免费查看。这是Google官方数据,反映真实用户体验。但CrUX只给出汇总数据,不告诉你哪个交互慢。
更详细的数据需要RUM(Real User Monitoring)。用web-vitals库在浏览器端采集INP值并上报:
import { onINP } from 'web-vitals';
onINP((metric) => {
// 上报到你的分析服务
navigator.sendBeacon('/analytics', JSON.stringify({
name: 'INP',
value: metric.value,
rating: metric.rating,
id: metric.id
}));
});
实验室数据用Chrome DevTools的Performance面板。录制一段交互操作,查看Main线程的火焰图,找到长任务。Lighthouse也能测量INP,但数据是模拟的,不如现场数据准确。
web.dev建议的优化路径:先用PageSpeed Insights看CrUX数据确认有没有问题,再用RUM数据找到具体是哪个交互慢,最后用DevTools Performance面板在实验室环境复现和调试。
INP优化优先级建议
别想着一次把所有交互都优化到200毫秒以内。按这个优先级来:
- 先看CrUX数据,如果75百分位INP超过500毫秒,说明有严重问题,优先修复
- 找到最慢的那个交互,通常是页面加载后的第一次点击或滚动
- 检查第三方脚本数量,砍掉不必要的
- 对剩余的长任务做拆分
- 优化完再测,循环迭代
INP优化是个持续过程,不是一次性工程。每次加新功能、新脚本都可能影响INP。建议把INP监控加入CI流程,每次发布前用Lighthouse跑一遍。
0 条评论