重任务性能优化的最佳实践

前言

在前端开发中,我们经常遇到一些计算量大的任务:数据处理、图表预计算、循环遍历、密集数学运算等。如果不小心放在主线程中同步执行,很容易导致:

  • 页面卡顿、掉帧;
  • 动画不流畅;
  • 用户点击无响应;
  • “页面无响应”警告。

本文将总结常见导致渲染卡顿的原因,并提供几种主流的优化思路,帮助你轻松搞定性能瓶颈!


一、问题再现:百万次循环,页面瞬间卡死

1
2
3
4
5
let total = 0;
for (let i = 0; i < 1_000_000; i++) {
total += i;
}
count.value = total;

为什么?

  • 这段代码是同步执行的;
  • JavaScript 单线程运行,会阻塞主线程
  • 在阻塞期间,浏览器无法渲染页面或响应用户操作
  • 用户体验极差,轻则掉帧,重则直接页面“假死”。

二、浏览器的工作原理(简化)

  • 浏览器每秒尝试绘制 60 帧(即每帧 ≈ 16.67ms);
  • 如果 JavaScript 在某一帧中执行耗时超过 16.67ms,下一帧就来不及渲染
  • 连续的掉帧就会产生“卡顿”体验。

三、优化方案总结

1. 分片执行封装(基于 requestAnimationFrame)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export function runHeavyTaskInChunks(taskFn, total, chunkSize = 1000, onDone) {
let index = 0;
function step() {
const end = Math.min(index + chunkSize, total);
for (; index < end; index++) {
taskFn(index);
}
if (index < total) {
requestAnimationFrame(step);
} else if (onDone) {
onDone();
}
}
requestAnimationFrame(step);
}

使用示例:

1
2
3
4
5
6
7
let total = 0;
runHeavyTaskInChunks(
(i) => { total += i; },
1_000_000,
10000,
() => { count.value = total; }
);

2. requestIdleCallback:后台空闲时计算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let i = 0;
let total = 0;

function processChunk(deadline) {
while (i < 1_000_000 && deadline.timeRemaining() > 0) {
total += i++;
}

if (i < 1_000_000) {
requestIdleCallback(processChunk);
} else {
count.value = total;
}
}

requestIdleCallback(processChunk);

3. Web Worker:真正的多线程执行

worker.js

1
2
3
4
5
6
7
self.onmessage = () => {
let total = 0;
for (let i = 0; i < 1_000_000; i++) {
total += i;
}
self.postMessage(total);
};

主线程使用

1
2
3
4
5
const worker = new Worker(new URL('./worker.js', import.meta.url));
worker.onmessage = (e) => {
count.value = e.data;
};
worker.postMessage('start');

4. 缓存计算结果

1
2
3
4
5
6
7
8
9
10
11
const cache = new Map();

function expensiveCalc(n) {
if (cache.has(n)) return cache.get(n);

let total = 0;
for (let i = 0; i < n; i++) total += i;

cache.set(n, total);
return total;
}

四、各种优化方案对比

方案 是否阻塞主线程 执行控制 适用场景
同步执行 快速执行 小任务或立即计算
分片(RAF)执行 较灵活 与 UI 同步,逐帧计算
requestIdleCallback 不可控 不紧急、后台计算
Web Worker 完全可控 高计算密度,大数据处理
缓存优化 快速返回 可复用结果、函数纯净

五、总结:根据场景选择

  • 如果是UI相关、用户交互触发的任务:用 requestAnimationFrame 分帧处理;
  • 如果是懒加载、后台逻辑:用 requestIdleCallback
  • 如果是重型 CPU 计算:果断上 Web Worker;
  • 如果任务可复用:加缓存最简单。