深入理解 requestIdleCallback:浏览器空闲时段的性能优化利器

发布于:2025-05-18 ⋅ 阅读:(16) ⋅ 点赞:(0)

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() 安排的回调。 

工作原理

  1. 浏览器在每一帧渲染完成后会检查是否有空闲时间

  2. 如果有空闲时间,且存在待执行的 idle 回调,则执行它们

  3. 每次 idle 回调执行时,可以通过 timeRemaining() 检查剩余时间

  4. 如果任务未完成,可以在回调中再次调用 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 参数强制在指定时间后执行(避免长期等待)

适用场景

  1. 日志上报和分析:将非关键的日志发送推迟到空闲时间

  2. 预加载资源:预加载接下来可能需要的非关键资源

  3. 大数据处理:分块处理大型数据集,避免界面卡顿

  4. 非关键UI更新:如更新界面上的辅助信息或统计数字

注意事项

  1. 不要用于关键任务:空闲回调可能永远不会执行,或者执行得很晚

  2. 任务应该可分片:每次回调应该只处理一小部分工作

  3. 避免DOM操作:在空闲回调中进行DOM操作可能触发重排/重绘

  4. 超时设置要合理:过短的 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及以下)


兼容性风险点列表

  1. 移动端注意

    • 部分安卓 WebView(特别是 Hybrid 应用内嵌浏览器)可能不支持

  2. Safari 特殊性

    • 某些版本即使支持 API,空闲时间计算可能不准确

  3. 隐身模式影响

    • 部分浏览器在隐身模式下会限制后台任务执行

兼容性解决方案列表

特性检测标准写法

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:视觉相关任务使用 requestAnimationFrame
  if (isVisualTask && 'requestAnimationFrame' in window) {
    id = window.requestAnimationFrame(() => {
      callback({
        timeRemaining: () => Math.max(0, 16.6 - (Date.now() - start)),
        didTimeout: Date.now() - start >= timeout
      });
    });
  }
  // 方案2:通用任务使用 setTimeout
  else {
    // 计算合理延迟时间(避免过度消耗资源)
    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>

网站公告

今日签到

点亮在社区的每一天
去签到