JavaScript 执行性能调优:从长任务拆分到事件循环优化
最常见的性能问题不是"网络慢"或"资源大",而是"JavaScript 跑了好久,页面卡住了"。
浏览器的主线程同时处理:JavaScript、样式、布局、绘制、合成。一旦 JavaScript 任务太长,就会阻塞后续的渲染,导致用户感受到的延迟。
1. 事件循环(Event Loop)简述
浏览器每一帧的执行顺序:
┌─────────────────────────┐
│ 执行所有同步 JS │ ← 你的代码在这
│ (Script 标签、事件处理) │
└─────────────────────────┘
↓
┌─────────────────────────┐
│ 执行 Microtask Queue │
│ (Promise、MutationObs) │
└─────────────────────────┘
↓
┌─────────────────────────┐
│ 渲染一帧 │
│ (样式、布局、画、合成) │
└─────────────────────────┘
↓
┌─────────────────────────┐
│ 执行 Macrotask Queue │
│ (setTimeout、click) │
└─────────────────────────┘
关键:如果步骤 1(JavaScript)跑了超过 50ms,就会延迟步骤 3(渲染),导致掉帧。
2. 什么是"长任务"?
浏览器认为"长任务"是指超过 50ms 的 JavaScript 执行。
// ❌ 长任务
function processData() {
for (let i = 0; i < 1000000000; i++) {
// 复杂计算,耗时 200ms
Math.sqrt(i)
}
}
// 调用这个函数
processData()
// 在这 200ms 内,用户的点击无法响应
// 屏幕也看不到更新
3. 长任务拆分:最常用的优化技术
方法 1:setTimeout 拆分
// 好一点,但不专业
function processDataInChunks() {
const data = Array.from({ length: 1000000 })
let index = 0
function processChunk() {
const end = Math.min(index + 10000, data.length)
for (let i = index; i < end; i++) {
// 处理一小块
}
index = end
if (index < data.length) {
setTimeout(processChunk, 0) // 让出主线程
}
}
processChunk()
}
这样每 10000 项用 setTimeout 延迟一下,主线程可以处理其他任务。
方法 2:requestIdleCallback(更好)
const data = Array.from({ length: 1000000 })
let index = 0
function processInIdle() {
requestIdleCallback((deadline) => {
// deadline.timeRemaining() 返回这一帧还有多少空闲时间
while (index < data.length && deadline.timeRemaining() > 1) {
// 处理一项
index++
}
if (index < data.length) {
processInIdle() // 下一帧继续
}
})
}
processInIdle()
requestIdleCallback 会在浏览器真正空闲时运行,不会浪费时间。
方法 3:scheduler.yield(最新方案)
// Chrome 123+, 实验性
async function processDataYield() {
for (let i = 0; i < 1000000; i++) {
// 每 1000 项让出一次
if (i % 1000 === 0) {
await scheduler.yield()
}
// 处理
}
}
4. Web Worker:把计算移出主线程
对于特别重的计算(ml.js、复杂算法),可以用 Web Worker 在后台线程运行:
MainThread.js:
const worker = new Worker('worker.js')
// 发送数据给 worker
worker.postMessage({
command: 'process',
data: hugeArray
})
// 监听 worker 的结果
worker.onmessage = (e) => {
console.log('处理完成', e.data)
}
worker.js:
self.onmessage = (e) => {
if (e.data.command === 'process') {
// 长时间计算,不会阻塞主线程
const result = heavyComputation(e.data.data)
self.postMessage(result)
}
}
function heavyComputation(data) {
// 耗时的 AI、加密、数据处理
return data.map(...)
}
优势:
- 主线程不会被阻塞
- 用户交互始终响应
缺点:
- 有通信开销
- Worker 不能操作 DOM
- 内存占用(每个 Worker 是独立线程)
5. MessageChannel 优雅地拆分任务
有时候 Promise.then() 优先级太高(Microtask),setTimeout 太不可控。用 MessageChannel 可以获得更精确的控制:
const channel = new MessageChannel()
const port = channel.port1
port.onmessage = () => {
console.log('继续下一个任务')
// 这会在本轮 Macrotask 之后,下一个 Macrotask 之前运行
}
function breakLongTask() {
// 做一个小块任务
doSomeWork()
// 发送消息给自己,触发下一个任务
channel.port2.postMessage(null)
}
MessageChannel 插在 Macrotask 之间,比 setTimeout 更高效。
6. 优化策略速查表
| 场景 | 方案 | 优先级 |
|---|---|---|
| 简单的数据处理 | 用 async/await + requestIdleCallback | 中 |
| CPU 密集型计算(ML、图像处理) | Web Worker | 高 |
| 频繁小任务的拆分 | MessageChannel | 高 |
| 需要 DOM 更新的逐步操作 | setTimeout + requestAnimationFrame | 中 |
| 依赖 API 的异步流程 | async/await | 中 |
7. 常见失败案例
案例 1:过度拆分反而变慢
// ❌ 太细致了,通信开销太大
for (let i = 0; i < list.length; i++) {
await scheduler.yield()
processOne(list[i])
}
应该分块处理,而不是每一项都让出。
案例 2:Worker 的数据太大
发送 10MB 的数据给 Worker,光是序列化-反序列化就要几百毫秒。
解决:用 Transferable Objects(零拷贝):
const buffer = new ArrayBuffer(10 * 1024 * 1024)
worker.postMessage({ buffer }, [buffer]) // 转移所有权,不复制
案例 3:忘记监听 Worker 全局异常
worker.addEventListener('error', (e) => {
console.error('Worker error:', e.message)
})
8. 性能监测与观察
用 Chrome DevTools 观察长任务
- Performance tab
- 点 record
- 做操作
- 停止,看时间轴,找紫色块(Long Task)
用代码观察:
// Long Task API (实验性)
const observer = new PerformanceObserver((list) => {
for (const {name, duration} of list.getEntries()) {
if (duration > 50) {
console.warn(`Long task: ${name} (${duration.toFixed(2)}ms)`)
}
}
})
observer.observe({ entryTypes: ['longtask'] })
9. 最佳实践清单
- 识别 JS 中的长任务(性能 tab 看紫块)
- 对 >50ms 的同步计算进行拆分
- 用
requestIdleCallback处理非关键数据 - 对 CPU 密集型计算考虑 Web Worker
- 定期检查
INP(Interaction to Next Paint)指标 - 在移动设备上测试(主线程瓶颈更明显)


