长任务拆分与调度策略:从 Long Tasks 到 INP 的前端交互提速实战
用户对“卡”的感受通常来自两件事:
- 点了没反应(交互延迟)
- 动画掉帧(滚动/拖拽/输入不跟手)
从工程角度看,这两者经常有同一个根因:主线程被长任务占满。
长任务(Long Task)通常指:主线程上连续执行超过 50ms 的任务。 一旦主线程被占用,浏览器就没法及时处理输入事件、也没法尽快绘制下一帧,最终表现为 INP 变差。
本文给你一套“从定位到修复”的实战路线:先找到是谁在占主线程,再用拆分与调度把交互路径变短。
1. Long Tasks 与 INP 的关系:为什么“看起来不大”的逻辑也会卡
INP 衡量的是:用户交互到页面下一次绘制的延迟。
当主线程忙时,交互会排队:
- 输入事件来了,但 JS 正在跑
- 事件处理函数要等前面的任务结束才能执行
- 执行完还要等渲染与绘制
所以 INP 的优化核心不是“把事件处理函数写得更短”这么简单,而是:
确保交互发生时,主线程有足够空闲。
2. 定位:先抓到长任务发生在哪里
2.1 DevTools Performance:最可靠
- 打开 Performance 面板
- 录制一次“卡顿场景”(点击、输入、滚动)
- 查看 Main 线程上的长任务(通常会有标记)
你需要回答:
- 长任务发生在交互前还是交互后?
- 是某个组件渲染导致,还是某段业务计算导致?
- 是否第三方脚本/埋点在抢主线程?
2.2 典型长任务来源
- 大 JSON 解析与遍历
- 大列表渲染(一次性渲染大量节点)
- 复杂的 diff / 频繁状态更新
- 第三方脚本(埋点、广告、客服)
3. 修复策略总览:三种最常用的方向
- 移出交互路径:把重逻辑从“点击处理”挪走
- 拆分成小片:让每一片都能在一帧内完成
- 移出主线程:Web Worker / OffscreenCanvas 等
你不需要一次用齐所有策略,先从最有效的一条开始。
4. 把重逻辑移出交互路径(最优先)
交互路径里应该只做:
- 必要的状态更新
- 必要的渲染触发
重计算可以:
- 提前算(预计算)
- 延后算(异步/空闲时算)
- 后台算(worker)
示例(思路示意):
// 点击时只做最小状态切换
function onClick() {
state.open = true
// 重计算延后
queueMicrotask(() => {
expensiveCompute()
})
}
这只是示意。更常见的做法是:把重逻辑从点击处理里拆出来,并在“页面空闲”或“数据到达时”提前准备。
5. 拆分成小片:按帧分片(time slicing)
当你必须在主线程完成某个大任务(例如处理大量数据),就要做分片。
5.1 使用 requestAnimationFrame 分片
function processInChunks(items: any[], chunkSize = 200) {
let index = 0
function run() {
const end = Math.min(index + chunkSize, items.length)
for (; index < end; index++) {
// 处理 items[index]
}
if (index < items.length) {
requestAnimationFrame(run)
}
}
requestAnimationFrame(run)
}
优点:
- 每帧做一点
- 页面不会长时间失去响应
5.2 使用 requestIdleCallback(注意兼容与超时)
function scheduleIdleWork(work: () => void) {
if ("requestIdleCallback" in window) {
;(window as any).requestIdleCallback(work, { timeout: 200 })
} else {
setTimeout(work, 0)
}
}
requestIdleCallback 适合非关键任务(预计算、缓存构建)。
6. 移出主线程:Web Worker 是最稳的工程手段
当任务属于:
- 数据解析
- 复杂计算
- 文本处理
优先考虑 Worker。
示意:
// main
const worker = new Worker(new URL("./worker.ts", import.meta.url), { type: "module" })
worker.postMessage({ items })
worker.onmessage = (e) => {
state.result = e.data
}
Worker 的关键收益是:
- 主线程不被计算占用
- 交互与渲染更顺
注意:
- 数据传输要考虑序列化成本(必要时用 transferable)
7. 调度与优先级:让“重要的事”先完成
现代浏览器提供了更丰富的调度能力(支持情况逐步完善),例如:
scheduler.postTask(按优先级调度)
即便你不使用这些新 API,也可以自己做一个轻量队列:
- 高优先级:交互相关
- 中优先级:首屏渲染后需要的任务
- 低优先级:预加载、预计算、日志上报
核心原则:
不要让低价值任务占用交互窗口期。
8. 可复制的排障清单(INP 变差时先做这些)
- Performance 录制:找到长任务与来源
- 检查交互路径:点击/输入处理是否做了重计算
- 检查渲染:是否一次性渲染了大量 DOM
- 检查第三方脚本:是否在关键交互时段运行
- 对重计算做分片或移入 worker
- 对非关键任务延后到 idle
结语:INP 优化的本质是“让主线程更空闲”
当你把长任务拆掉、把重逻辑移出交互路径、把计算移出主线程,交互体验会明显改善。
建议你把这篇与以下两篇组合成一条闭环:
- TTFB(服务端响应)→ 影响加载
- 资源优先级(Priority Hints)→ 影响 LCP
- 长任务拆分(调度策略)→ 影响 INP


