← 中文资源

简体中文版本

INP 不达标 — 长任务阻塞输入处理

通过找到延迟事件处理器的长任务、使用 scheduler.yield 与代码分割、拆分渲染阻塞工作,来改进 Interaction to Next Paint。

阅读英文原文

症状

  性能录制把等待具体化:输入事件落地,随后一个 180ms 的任务先跑完才有下一帧。PerformanceObserver 给你清单——longtask 214ms 来自 app.js,与交互窗口重叠的就是阻塞者。任务内部用 Bottom-Up 视图区分框架重渲染与自有计算,修哪一侧取决于谁占大头。CrUX 的 INP 落在 poor 档而其他 Core Web Vitals 通过、点击筛选或搜索输入手感迟滞,是同一件事的用户侧与数据侧。

诊断思路

  先录制交互,看输入事件与下一次绘制之间的间隙被哪个任务填满;再用 longtask 观察者拿任务时长与来源脚本;最后进任务内部分解,自时间排名指明修框架渲染还是修自己的循环。

根因

  INP 度量输入到下一次绘制的延迟。输入时刻主线程正执行长任务、或处理器本身在让出之前做重同步工作,输入就要等、绘制排在后面——交互延迟就此变差。

修复与验证

  把处理器拆成“即时反馈 + 延迟重活”:先 setLoading(true) 让绘制发生,await scheduler.yield() 让浏览器处理输入与绘制,再跑分块的重活(每块 5000 行,块间让出);代码分割让兆级代码不在用户点击的时刻解析执行;大列表虚拟化、渲染路径 memo 化,让输入触发最小重渲染。验证时录制中处理器在首帧内启动、重活跨帧让出、CrUX 现场 INP 进入 good 档。把全部工作都推迟会引入双提交等逻辑竞态——为绘制让出、再跑逻辑,并配显式状态守卫。

代码示例

new PerformanceObserver((l) =>
  l.getEntries().forEach((e) =>
    console.log(e.name, Math.round(e.duration),
                e.attribution?.[0]?.containerSrc))
).observe({ entryTypes: ['longtask'] });

// longtask 214 app.js
button.addEventListener('click', async () => {
  setLoading(true);                      // paint happens here
  const yieldToBrowser =
    scheduler.yield ? scheduler.yield() : new Promise(r => setTimeout(r));
  await yieldToBrowser;                  // input, paint, then heavy work
  await processRows(rows);               // chunked internally
  setLoading(false);
});
async function processRows(rows) {
  for (let i = 0; i < rows.length; i += 5000) {
    work(rows.slice(i, i + 5000));
    await (scheduler.yield?.() ?? new Promise(r => setTimeout(r)));
  }
}