背景 如果让一个耗时且资源消耗大的任务占用主线程,很容易破坏网站的用户体验。无论应用程序变得多复杂,事件循环一次仍然只能处理一件事。如果你的代码占用了它,其他所有操作都将处于待机状态,通常用户很快就会察觉到。
一个任务耗时过长,会导致页面卡顿、掉帧,甚至出现假死的现象!!!
分解方案 1. setTimeout + 递归 setTimeout的 4ms 最小延迟。 也就是说,即使你设置了 0 毫秒,实际也要等待至少 4 毫秒之后才会被执行。 这是因为 JavaScript 引擎在处理事件循环时,会有一些额外的延迟。也是防止高频率的定时器拖垮页面性能。
虽然你写的是 setTimeout(fn, 0),表示“立刻执行”,但实际上浏览器会有一个最小延迟阈值
a、逐步计算 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 function sumNumbers (start, end, step, sum = 0 ) { if (start >= end) { console .log("Total Sum:" , sum); return ; } let nextEnd = Math .min(start + step, end); for (let i = start; i < nextEnd; i++) { sum += i; } console .log(`Processed: ${start} to ${nextEnd} ` ); setTimeout(() => sumNumbers(nextEnd, end, step, sum), 0 ); } sumNumbers(1 , 1000000000 , 1000000 );
优点:
每次只计算一小部分,避免主线程长时间被占用。
UI 仍然可以响应,用户不会感到页面卡顿。
b、分批处理数组 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function processArrayInChunks (arr, chunkSize, index = 0 ) { if (index >= arr.length) { console .log("Processing done!" ); return ; } let end = Math .min(index + chunkSize, arr.length); for (let i = index; i < end; i++) { console .log("Processing:" , arr[i]); } setTimeout(() => processArrayInChunks(arr, chunkSize, end), 0 ); } let bigArray = Array .from({ length : 10000 }, (_, i) => i);processArrayInChunks(bigArray, 1000 );
优点:
适用于超大数组,防止页面卡死
允许 UI 更新,用户可见进度
c、逐步渲染 DOM 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 function renderListInChunks (container, data, chunkSize, index = 0 ) { if (index >= data.length) { console .log("Rendering complete!" ); return ; } let fragment = document .createDocumentFragment(); let end = Math .min(index + chunkSize, data.length); for (let i = index; i < end; i++) { let item = document .createElement("div" ); item.textContent = `Item ${data[i]} ` ; fragment.appendChild(item); } container.appendChild(fragment); setTimeout(() => renderListInChunks(container, data, chunkSize, end), 0 ); } let container = document .getElementById("list-container" );let data = Array .from({ length : 10000 }, (_, i) => i);renderListInChunks(container, data, 100 );
优点:
避免 UI 阻塞,每次渲染一部分,提高页面流畅度。
减少 DOM 操作成本,使用 documentFragment 批量插入。
2. Async/Await & Timeout 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 async function sumNumbers (start, end, step, sum = 0 ) { for (let i = start; i < end; i += step) { for (let j = i; j < Math .min(i + step, end); j++) { sum += j; } console .log(`Processed: ${i} to ${Math .min(i + step, end)} ` ); await new Promise (resolve => setTimeout(resolve, 0 )); } console .log("Total Sum:" , sum); } sumNumbers(1 , 1000000000 , 1000000 );
原理:
每次累加 一部分数据,然后 await 一个 setTimeout(0) 让出主线程。
让浏览器有机会渲染 UI,防止页面冻结。
3. scheduler.postTask() 使用场景: 渐进式渲染(如大列表)、分批数据处理、避免页面卡顿、UI 优先、任务退让
不幸的是,整个调度器接口存在一个缺陷:目前它在所有浏览器中的支持情况并不理想。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 async function processInChunks (data, chunkSize ) { let index = 0 ; while (index < data.length) { const chunk = data.slice(index, index + chunkSize); chunk.forEach(item => { console .log("Processing:" , item); }); index += chunkSize; await scheduler.postTask(() => {}, { priority : 'user-visible' }); } console .log("All chunks processed." ); } const data = Array .from({ length : 10000 }, (_, i) => i);processInChunks(data, 1000 );
4. requestIdleCallback requestIdleCallback() 是一个浏览器提供的 API,用来在浏览器空闲时执行代码。它的设计初衷是让我们可以安排一些不重要但又需要执行的任务 (例如:打点、预处理数据、缓存更新等),而不会影响页面的流畅度。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 window .requestIdleCallback = window .requestIdleCallback || function (cb ) { return setTimeout(() => { cb({ timeRemaining: () => 0 , didTimeout: true }); }, 1 ); }; window .cancelIdleCallback = window .cancelIdleCallback || function (id ) { clearTimeout(id); }; let bigArray = Array .from({ length : 10000 }, (_, i) => i);function processChunk (deadline ) { while (deadline.timeRemaining() > 0 && bigArray.length > 0 ) { let item = bigArray.shift(); console .log("Processing:" , item); } if (bigArray.length > 0 ) { requestIdleCallback(processChunk); } else { console .log("All done!" ); } } requestIdleCallback(processChunk);
5. requestAnimationFrame requestAnimationFrame() 是 JavaScript 中专为高性能动画渲染设计的 API。它会在浏览器下一次重绘前执行回调,确保动画平滑不卡顿,是做 动画、游戏循环、进度更新 的黄金工具。
注意事项
不要滥用:不是所有长任务都适合 requestAnimationFrame,它适用于视觉相关的更新
每一帧必须快速返回,不超过 16ms,否则掉帧
可以与 cancelAnimationFrame 配对使用来停止动画
6. MessageChannel 我们可以用它像微任务一样分片处理大任务,它比 setTimeout 更快响应、不卡 UI。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 const channel = new MessageChannel();const taskQueue = [];channel.port1.onmessage = () => { const task = taskQueue.shift(); if (task) task(); }; function schedule (task ) { taskQueue.push(task); channel.port2.postMessage(null ); } let arr = Array .from({ length : 10000 }, (_, i) => i);function processChunk ( ) { const chunk = arr.splice(0 , 500 ); chunk.forEach(i => { console .log("Processing:" , i); }); if (arr.length > 0 ) { schedule(processChunk); } else { console .log("All done!" ); } } schedule(processChunk);
7. Web Workers 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 const worker = new Worker('worker.js' );const bigArray = Array .from({ length : 10000 }, (_, i) => i);worker.postMessage({ type : 'start' , data : bigArray }); worker.onmessage = (e ) => { if (e.data.done) { console .log("处理完成" ); } else { console .log("处理中:" , e.data.chunkResult); } }; self.onmessage = function (e ) { if (e.data.type === 'start' ) { const data = e.data.data; const CHUNK_SIZE = 1000 ; let index = 0 ; function processNextChunk ( ) { const chunk = data.slice(index, index + CHUNK_SIZE); const chunkResult = chunk.map(i => i * 2 ); postMessage({ chunkResult }); index += CHUNK_SIZE; if (index < data.length) { setTimeout(processNextChunk, 0 ); } else { postMessage({ done : true }); } } processNextChunk(); } };
文章来来源于