症状
性能录制把等待具体化:输入事件落地,随后一个 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)));
}
}