背景 如果让一个耗时且资源消耗大的任务占用主线程,很容易破坏网站的用户体验。无论应用程序变得多复杂,事件循环一次仍然只能处理一件事。如果你的代码占用了它,其他所有操作都将处于待机状态,通常用户很快就会察觉到。
一个任务耗时过长,会导致页面卡顿、掉帧,甚至出现假死的现象!!!
分解方案 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 (); } };
[文章来来源于](https://mp.weixin.qq.com/s/CYWm7y_qyanZb0c4s29sag)