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跑一遍。

分类: seo

0 条评论

发表回复

Avatar placeholder

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