requestIdleCallback 核心作用
requestIdleCallback
?是瀏覽器提供的 API,用于將非關鍵任務延遲到瀏覽器空閑時段執行,避免阻塞用戶交互、動畫等關鍵任務,從而提升頁面性能體驗。
基本語法
const handle = window.requestIdleCallback(callback[, options])
參數
-
callback:一個將在瀏覽器空閑時期被調用的函數。該回調函數接收一個參數:
-
IdleDeadline
?對象,包含:-
timeRemaining()
:返回當前幀剩余的空閑時間(毫秒),通常 ≤ 50ms -
didTimeout
:布爾值,表示是否因為指定的 timeout 時間已到而觸發回調
-
-
-
options(可選):配置對象
-
timeout
:如果指定了 timeout,并且回調在 timeout 毫秒后還沒有被調用,則回調會在下一次有機會時被強制執行
-
返回值
返回一個 ID,可以傳遞給?cancelIdleCallback()
?來取消回調。
配套方法
window.cancelIdleCallback(handle)
取消之前通過?requestIdleCallback()
?安排的回調。?
工作原理
-
瀏覽器在每一幀渲染完成后會檢查是否有空閑時間
-
如果有空閑時間,且存在待執行的 idle 回調,則執行它們
-
每次 idle 回調執行時,可以通過?
timeRemaining()
?檢查剩余時間 -
如果任務未完成,可以在回調中再次調用?
requestIdleCallback
?繼續處理
使用示例
基本用法
function processInIdleTime(deadline) {while (deadline.timeRemaining() > 0 && tasks.length > 0) {performTask(tasks.pop());}if (tasks.length > 0) {requestIdleCallback(processInIdleTime);}
}requestIdleCallback(processInIdleTime);
帶超時的用法
requestIdleCallback(processInIdleTime, { timeout: 2000 });
// 保證在2秒內執行,即使瀏覽器一直不空閑
關鍵特性
特性 | 說明 |
---|---|
空閑期執行 | 只在瀏覽器主線程空閑時運行(每幀渲染后的空閑時間) |
可中斷性 | 如果用戶開始交互,任務會被暫停 |
超時控制 | 可通過?timeout ?參數強制在指定時間后執行(避免長期等待) |
適用場景
-
日志上報和分析:將非關鍵的日志發送推遲到空閑時間
-
預加載資源:預加載接下來可能需要的非關鍵資源
-
大數據處理:分塊處理大型數據集,避免界面卡頓
-
非關鍵UI更新:如更新界面上的輔助信息或統計數字
注意事項
-
不要用于關鍵任務:空閑回調可能永遠不會執行,或者執行得很晚
-
任務應該可分片:每次回調應該只處理一小部分工作
-
避免DOM操作:在空閑回調中進行DOM操作可能觸發重排/重繪
-
超時設置要合理:過短的 timeout 會使 API 失去意義,過長則影響體驗
瀏覽器兼容性分析
? 完全支持的瀏覽器
-
Chrome
-
版本:47+(2015年發布)
-
備注:包括所有基于 Chromium 的瀏覽器(Edge、Opera 等)
-
-
Firefox
-
版本:55+(2017年發布)
-
備注:在移動端和桌面端表現一致
-
-
Edge
-
版本:79+(Chromium 內核版本)
-
?? 部分支持/行為差異的瀏覽器
-
Safari
-
版本:部分支持(需檢測)
-
問題:
-
iOS Safari 和 macOS Safari 實現可能不一致
-
某些版本中?
timeRemaining()
?返回值不準確
-
-
? 不支持的瀏覽器
-
Internet Explorer
-
所有版本均不支持
-
-
舊版 Edge(EdgeHTML 內核)
-
版本:18 及以下
-
-
Android 默認瀏覽器(4.4及以下)
兼容性風險點列表
-
移動端注意
-
部分安卓 WebView(特別是 Hybrid 應用內嵌瀏覽器)可能不支持
-
-
Safari 特殊性
-
某些版本即使支持 API,空閑時間計算可能不準確
-
-
隱身模式影響
-
部分瀏覽器在隱身模式下會限制后臺任務執行
-
兼容性解決方案列表
特性檢測標準寫法
const hasIdleCallback = 'requestIdleCallback' in window;
推薦降級方案
優先降級到 requestAnimationFrame(適合視覺相關任務)
其次降級到 setTimeout(callback, 0)(通用方案)
Polyfill 選擇
官方推薦的 polyfill
注意:polyfill 無法真正模擬空閑期,只是延遲執行
/*** 增強型空閑任務調度器(支持多級降級方案)* @param {Function} callback - 需要執行的回調函數,接收 deadline 對象* @param {Object} [options] - 配置選項* @param {number} [options.timeout=0] - 超時時間(毫秒)* @returns {number} 調度器ID(可用于取消)*/
function enhancedRequestIdleCallback(callback, options = {}) {// 參數有效性檢查if (typeof callback !== 'function') {throw new TypeError('回調必須是函數');}const { timeout = 0 } = options;// 原生支持檢測if ('requestIdleCallback' in window) {return window.requestIdleCallback(callback, { timeout });}// ========== 降級方案實現 ==========let id;const start = Date.now();const isVisualTask = isRelatedToVisualUpdate(callback);// 方案1:視覺相關任務使用 requestAnimationFrameif (isVisualTask && 'requestAnimationFrame' in window) {id = window.requestAnimationFrame(() => {callback({timeRemaining: () => Math.max(0, 16.6 - (Date.now() - start)),didTimeout: Date.now() - start >= timeout});});}// 方案2:通用任務使用 setTimeoutelse {// 計算合理延遲時間(避免過度消耗資源)const delay = calculateSafeDelay(isVisualTask);id = window.setTimeout(() => {callback({timeRemaining: () => 1, // 模擬1ms剩余時間didTimeout: true // 降級模式下總是觸發超時});}, delay);}// 添加超時強制觸發機制if (timeout > 0) {const timeoutId = setTimeout(() => {callback({timeRemaining: () => 0,didTimeout: true});clearTimeout(id);}, timeout);// 返回復合ID用于取消return { rId: id, tId: timeoutId };}return id;
}/*** 取消空閑任務調度* @param {number|Object} id - 調度器返回的ID*/
function enhancedCancelIdleCallback(id) {if ('cancelIdleCallback' in window) {window.cancelIdleCallback(id);return;}// 處理復合ID(超時場景)if (typeof id === 'object') {clearTimeout(id.tId);id = id.rId;}// 根據降級方案取消if ('cancelAnimationFrame' in window) {window.cancelAnimationFrame(id);} else {clearTimeout(id);}
}// ========== 工具函數 ==========
/*** 判斷任務是否與視覺更新相關* (根據常見DOM API使用模式推測)*/
function isRelatedToVisualUpdate(fn) {const fnStr = fn.toString();return /(offset|scroll|client|getBounding|style)/.test(fnStr);
}/*** 計算安全延遲時間* 視覺任務:下一幀時間(16.6ms)* 非視覺任務:分級延遲(0-50ms隨機)*/
function calculateSafeDelay(isVisual) {return isVisual ? 16 : Math.min(50, Math.floor(Math.random() * 50));
}// ========== 使用示例 ==========
// 示例任務
function backgroundTask(deadline) {while (deadline.timeRemaining() > 0) {// 執行任務分片...}if (hasMoreWork) {enhancedRequestIdleCallback(backgroundTask);}
}// 啟動任務
const taskId = enhancedRequestIdleCallback(backgroundTask, { timeout: 2000 });// 取消任務
// enhancedCancelIdleCallback(taskId);
注意事項
避免在回調中修改 DOM(可能觸發重排)
空閑時間不保證,任務應有中斷/恢復機制
耗時任務應使用 Web Worker
requestIdleCallback
的詳細介紹和示例代碼
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>requestIdleCallback 示例與兼容性處理</title>
</head>
<body>
<h1>requestIdleCallback 演示</h1>
<div id="output"></div><script>/*** 兼容性處理:如果原生不支持 requestIdleCallback,* 使用 setTimeout 實現降級方案*/window.requestIdleCallback = window.requestIdleCallback || function(cb) {// 降級方案:用 50ms 延遲模擬空閑時段let start = Date.now();return setTimeout(function() {cb({didTimeout: false,timeRemaining: function() {// 確保至少留出 1ms 時間return Math.max(0, 50 - (Date.now() - start));}});}, 1);};/*** 分塊任務處理器* @param {Array} taskList - 要處理的任務數組* @param {Function} processor - 單個任務處理函數* @param {number} chunkSize - 每次處理的任務數(默認 10)*/function processTasksInIdle(taskList, processor, chunkSize = 10) {let index = 0;function doChunk(deadline) {// 當剩余時間 > 0 或超時前處理任務while ((deadline.timeRemaining() > 0 || deadline.didTimeout) &&index < taskList.length) {// 每次處理指定數量的任務const tasksToProcess = taskList.slice(index, index + chunkSize);tasksToProcess.forEach(task => processor(task));index += chunkSize;// 更新頁面顯示進度updateProgress(index);}// 如果還有剩余任務,繼續調度if (index < taskList.length) {// 使用超時參數 100ms 保證即使不空閑也會執行requestIdleCallback(doChunk, { timeout: 100 });}}// 初始調用requestIdleCallback(doChunk, { timeout: 100 });}// 示例:創建 500 個元素的列表(模擬大量任務)const dummyTasks = new Array(500).fill(null).map((_, i) => ({id: i + 1,content: `Item ${i + 1}`}));// 任務處理函數(模擬DOM操作)function handleTask(task) {const div = document.createElement('div');div.textContent = task.content;// 這里可以添加更復雜的操作}// 更新進度顯示function updateProgress(processedCount) {const output = document.getElementById('output');output.textContent = `已處理 ${processedCount}/${dummyTasks.length} 項任務`;}// 啟動任務處理(頁面加載完成后)window.addEventListener('load', () => {processTasksInIdle(dummyTasks, handleTask, 10); // 每次處理10個});
</script><!-- 兼容性提示 -->
<script>// 檢測是否原生支持if (!window.requestIdleCallback) {const warn = document.createElement('p');warn.style.color = 'red';warn.textContent = '當前瀏覽器不支持 requestIdleCallback,已使用 setTimeout 降級方案';document.body.appendChild(warn);}
</script>
</body>
</html>