前言
在前端开发中,我们经常遇到一些计算量大的任务:数据处理、图表预计算、循环遍历、密集数学运算等。如果不小心放在主线程中同步执行,很容易导致:
- 页面卡顿、掉帧;
- 动画不流畅;
- 用户点击无响应;
- “页面无响应”警告。
本文将总结常见导致渲染卡顿的原因,并提供几种主流的优化思路,帮助你轻松搞定性能瓶颈!
一、问题再现:百万次循环,页面瞬间卡死
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;
- 如果任务可复用:加缓存最简单。