什么是浏览器长任务?如何减少主线程阻塞?
简化版
Long Tasks API 将占用主线程 50ms 或以上的任务暴露为长任务。长任务会推迟输入处理和下一次绘制,恶化交互体验;应先用 Performance trace 定位脚本与渲染工作,再减少总计算、建立真正异步任务边界、虚拟化大量 DOM,或把不依赖 DOM 的重计算移到 Web Worker。
详细版
JavaScript 任务按 run-to-completion 执行,同一任务返回前浏览器不能插入另一个任务处理输入。把大函数拆成多个同步函数没有用;需要 scheduler.yield()(并准备兼容方案)、定时任务等方式把续作排到后续任务,给高优先级输入和渲染机会。
50ms 是 Long Tasks API 的观察阈值,不是“49ms 永远流畅”。60Hz 一帧约 16.7ms,交互还包含输入延迟、处理时间和呈现延迟;重复 30ms 任务同样可能造成明显问题。
Worker 适合 JSON/文件解析、图像和大数据计算,但不能直接操作 DOM,结构化克隆与消息传输有成本。优化要以用户交互和低端设备为场景,而非只看开发机单次函数耗时。
完整版教学
一、主线程为什么会让任务互相排队
浏览器主线程承担 JavaScript、事件回调、样式、布局和部分绘制协调。一个任务开始执行后通常要运行到调用栈清空,输入事件即使已经到达,也只能等待。
任务 A 80ms ──────────────────────────┐
用户点击(20ms到达) ──等待60ms─────────┼→ 处理点击 → 下一次绘制
┘
易错点:长任务是调度现象,不是“函数行数很多”;五个同步函数连着调用仍是同一个任务。
二、50ms 阈值怎样理解
Long Tasks API 报告持续 50ms 或以上的任务。超过前 50ms 的部分常被称为 blocking portion,例如 120ms 任务贡献约 70ms 阻塞部分,但用户实际输入延迟取决于输入到达时刻。
| 任务时长 | 是否长任务 | 超过 50ms 的部分 |
|---|---|---|
| 35ms | 否 | 0ms |
| 80ms | 是 | 30ms |
| 240ms | 是 | 190ms |
60Hz 屏幕一帧预算约 1000/60≈16.7ms,所以不到 50ms 的任务也可能丢帧。50ms 是 API 归类线,不是动画或 INP 的完整合格线。
三、真正的任务拆分必须把控制权还给浏览器
下面两个函数虽然代码分开,仍在同一同步调用栈中,不能处理插入的输入。真正切片要在批次间 yield,使续作成为后续任务。
async function processItems(items) {
let lastYield = performance.now()
for (const item of items) {
process(item)
if (performance.now() - lastYield >= 40) {
await yieldToMain() // 可封装 scheduler.yield 与兼容回退
lastYield = performance.now()
}
}
}
切得越细,调度和状态保存开销越高;切得太粗,响应性仍差。40ms 只是示意预算,应在目标设备上以输入与帧表现调节,不能当统一常量。
四、scheduler.yield、定时切片与 idle 各有什么边界
scheduler.yield() 能暂停当前异步流程并调度续作,但兼容范围必须检测;setTimeout 可作为更广泛的任务边界,最小延迟和优先级语义不同。两者都要 await 或明确续作,否则没有真正让出。
requestIdleCallback 适合可推迟的后台工作,不适合用户点击后的关键反馈;空闲繁忙时回调可能很晚。requestAnimationFrame 面向下一帧视觉更新,也不是放置几十毫秒重计算的容器。
| API | 适合 | 不适合 |
|---|---|---|
scheduler.yield() | 长流程分段续作 | 未检测兼容的唯一方案 |
setTimeout | 通用任务切片回退 | 精确帧调度 |
requestIdleCallback | 非紧急后台任务 | 紧急交互反馈 |
requestAnimationFrame | 视觉读写同步 | 大量计算 |
五、Worker 何时收益大于通信成本
Worker 在独立线程执行脚本,不直接阻塞主线程,适合 CPU 密集且不访问 DOM 的计算。数据通过结构化克隆传输,大对象复制可能昂贵;ArrayBuffer 可在适当场景使用 transferable 转移所有权。
假设主线程计算 180ms,发送与接收各 12ms,Worker 计算同为 180ms,主线程阻塞可从约 180ms 降到通信处理约 24ms,虽总完成时间未必明显下降,但交互更可用。若计算只有 3ms 而通信 8ms,迁移反而亏损。
Worker 也不能解决 DOM 创建、样式计算和框架提交阶段。应先把纯计算分离,再让主线程以小批次应用结果。
六、怎样定位并形成线上证据
Chrome Performance 面板可查看 Main 线程火焰图、长任务红色标记、脚本 URL 和渲染阶段。线上可通过 PerformanceObserver 采集 longtask 条目,但支持范围、归因信息和隐私限制需要按目标浏览器验证。
const observer = new PerformanceObserver(list => {
for (const entry of list.getEntries()) {
report({ name: entry.name, duration: entry.duration })
}
})
observer.observe({ type: 'longtask', buffered: true })
长任务数量不能直接等同 INP。INP 与真实交互有关,包含输入延迟、处理和呈现延迟;一个页面有启动长任务但用户从未交互,影响路径与交互时长仍需结合事件归因。
上线要按页面、release、设备和交互聚合。低端设备上 60ms 的任务在高端机可能只有 15ms,采样只覆盖旗舰设备会低估问题。
七、常见误区与追问
- 误区:把一个大函数拆成多个小函数就拆掉了长任务。 同步调用栈未结束,主线程仍不能处理其他任务。
- 误区:所有低于 50ms 的任务都不会卡顿。 50ms 是 Long Tasks API 阈值,动画帧预算更小,连续短任务也会拥塞。
- 误区:Web Worker 能优化所有前端慢操作。 Worker 不能直接操作 DOM,通信与序列化也有成本。
- 追问:为什么拆任务后总耗时可能变长? 多了调度和状态切换开销,换来的是响应性而非必然减少计算量。
- 追问:requestIdleCallback 适合处理点击后的计算吗? 不适合,紧急交互不能等待不确定的空闲时机。
- 追问:Long Task 与 INP 有什么关系? 长任务可能增加输入延迟或呈现延迟,但 INP 衡量实际交互,二者不等价。
- 追问:怎样决定移到 Worker? 比较计算阻塞、通信成本、数据大小,并确认任务不依赖 DOM。
八、加强记忆
用“找任务、真让出、算通信、看交互”处理主线程阻塞:50ms 是长任务观察线而非流畅保证,拆函数必须升级为任务边界,纯重计算才评估 Worker,DOM 与渲染仍在主线程治理;最后用真实交互的 INP、trace 和设备分群验证,而不是只数红色长任务。