INP 优化实战:别再只盯着 FID 了,这套组合拳把响应延迟压到 200ms 以内

INP 优化实战:别再只盯着 FID 了,这套组合拳把响应延迟压到 200ms 以内

导语

你的页面 LCP 已经压到 2.5s 了,CLS 也绿了,但用户还是觉得“卡”。问题很可能出在 INP 上——这个 2024 年 3 月正式接替 FID 的核心 Web 指标,衡量的是页面在整个生命周期内对点击、触摸、按键的响应速度。如果你的 INP 超过 200ms,哪怕页面“已经加载完”,用户也会觉得前端“不跟手”。

正文

问题:为什么 FID 退役了,INP 更难搞

FID 只看“第一次交互”的输入延迟,而 INP 覆盖整个页面生命周期的所有交互,取 P75 最差响应作为最终得分。也就是说,一个 500ms 的按钮点击、一次 800ms 的搜索联想,都可能把 INP 拉红。更麻烦的是,INP 只能用真实用户监控拿到,Lab 工具里经常测不出来。

根据 MDN 和 web.dev 的定义,INP 低于 200ms 为良好,200–500ms 需要改进,超过 500ms 响应较慢。多篇实战文章也提到:主线程阻塞、长任务、事件处理函数执行时间过长是 INP 三大杀手。

方案:四层优化策略

  • 第一层:主线程空闲控制:把重计算拆成多个小于 50ms 的任务块,给浏览器喘息机会。scheduler.postTask()、requestIdleCallback() 都可以用,但更实用的是 setTimeout 和 requestAnimationFrame 分片。
  • 第二层:长任务拆分:Chrome DevTools Performance 面板里超过 50ms 的任务就是 Long Tasks,它们是 INP 的头号杀手。用 performance.mark() 和 performance.measure() 标出长任务来源,优先优化第三方脚本和统计埋点代码。
  • 第三层:输入响应优化:不要在点击回调里同步做重计算或等待 fetch。改成“立即给视觉反馈 → 异步取数 → 再更新状态”的三段式。这符合 RAIL 模型里 100ms 内响应用户输入的规范。
  • 第四层:第三方与运行时治理:广告、统计、客服浮窗这些第三方脚本经常偷偷吃掉主线程。用 async 和 defer 控制加载时机,设置 content-visibility: auto 跳过视口外渲染。

代码:从“卡”到“跟手”的改造示例

改造前:

button.addEventListener("click", async () => {  const data = await fetch("/api/search?q=" + keyword)  const result = await data.json()  renderList(result)})

改造后:

button.addEventListener("click", async () => {  showLoadingSpinner()  await new Promise(requestAnimationFrame)  const data = await fetch("/api/search?q=" + keyword)  const result = await data.json()  renderList(result)})

这个改造的本质是:先把“我已经收到了”画出来,再把“我算完了”交给下一帧。INP 测的是“下一次绘制”的时间,不是“完整数据渲染”的时间。

结论:用 Lighthouse CI 把 INP 管起来

INP 是真实用户指标,Lab 工具只能辅助。建议用 Lighthouse CI 在每次提交时跑一遍,同时接真实用户监控抓线上 P75 值。优化优先级按这个顺序排:

  1. 主线程阻塞大于 50ms 的长任务
  2. 事件回调里同步 fetch 或重计算
  3. 未加 defer 的第三方脚本
  4. 连续布局抖动导致的渲染延迟

如果你的 INP 还在 500ms 以上,先去看 Performance 面板里的 Long Tasks,那是最直接的线索。

收尾

本周先做一件事:打开 Chrome DevTools 的 Performance 面板,录制一次用户真实操作,数一下有多少个超过 50ms 的长任务。先解决最长的那个,INP 通常会立刻改善一截。

评论区

0 条评论

登录后可评论。

阿速·性能优化 2476 阅读