利用requestIdleCallback优化Dom的更新性能

概述

​requestIdleCallback​ 是一个浏览器 API,允许在浏览器空闲时执行非关键任务,不会阻塞主线程。这对于提升用户体验和性能非常重要,特别是在处理大量 DOM 更新时。

实现

1. 工具函数 (idleCallback.ts​)

创建一个完整的 requestIdleCallback​ 工具库,包括:

  • requestIdleCallback: 在浏览器空闲时执行回调
  • cancelIdleCallback: 取消之前调度的回调
  • IdleTaskQueue: 批量处理任务的队列类

特性

  • ✅ 自动降级: 对于不支持 requestIdleCallback 的浏览器,自动降级为 setTimeout + MessageChannel
  • ✅ 超时保护: 支持超时配置,确保任务最终会被执行
  • ✅ 批量处理: IdleTaskQueue 可以在空闲时间内批量处理多个任务
typescript 复制代码
/**
 * requestIdleCallback 工具函数
 * 在浏览器空闲时执行回调,不会阻塞主线程
 * 提供降级方案以支持不支持 requestIdleCallback 的浏览器
 */

interface IdleCallbackOptions {
  timeout?: number; // 超时时间(毫秒),如果指定,回调会在超时后强制执行
}

interface IdleDeadline {
  didTimeout: boolean; // 是否因为超时而执行
  timeRemaining(): number; // 返回当前空闲时间(毫秒)
}

type IdleCallbackHandle = number;

/**
 * 在浏览器空闲时执行回调
 * @param callback 要执行的回调函数
 * @param options 配置选项
 * @returns 请求 ID,可用于取消
 */
export const requestIdleCallback = (
  callback: (deadline: IdleDeadline) => void,
  options?: IdleCallbackOptions
): IdleCallbackHandle => {
  // 如果浏览器支持原生 requestIdleCallback,直接使用
  if (typeof window !== 'undefined' && 'requestIdleCallback' in window) {
    return window.requestIdleCallback(callback, options);
  }

  // 降级方案:使用 setTimeout 模拟
  // 使用 1ms 延迟,让浏览器有机会处理其他任务
  const timeout = options?.timeout ?? 5000; // 默认 5 秒超时
  const startTime = Date.now();

  const timeoutId = setTimeout(() => {
    callback({
      didTimeout: true,
      timeRemaining: () => Math.max(0, 50 - (Date.now() - startTime)),
    });
  }, timeout);

  // 使用 MessageChannel 实现更接近 requestIdleCallback 的行为
  // MessageChannel 会在当前任务完成后、下一个任务之前执行
  const channel = new MessageChannel();
  channel.port1.onmessage = () => {
    clearTimeout(timeoutId);
    callback({
      didTimeout: false,
      timeRemaining: () => Math.max(0, 50 - (Date.now() - startTime)),
    });
  };
  channel.port2.postMessage(null);

  // 返回一个可以用于取消的 ID
  return timeoutId as unknown as IdleCallbackHandle;
};

/**
 * 取消之前通过 requestIdleCallback 调度的回调
 * @param handle 之前返回的请求 ID
 */
export const cancelIdleCallback = (handle: IdleCallbackHandle): void => {
  if (typeof window !== 'undefined' && 'cancelIdleCallback' in window) {
    window.cancelIdleCallback(handle);
  } else {
    // 降级方案:清除 setTimeout
    clearTimeout(handle as unknown as number);
  }
};

/**
 * 批量执行任务,在空闲时逐个处理
 * 适用于需要处理大量非关键任务的场景
 */
export class IdleTaskQueue {
  private tasks: Array<() => void> = [];
  private isProcessing = false;
  private currentHandle: IdleCallbackHandle | null = null;

  /**
   * 添加任务到队列
   */
  add(task: () => void): void {
    this.tasks.push(task);
    this.process();
  }

  /**
   * 批量添加任务
   */
  addBatch(tasks: Array<() => void>): void {
    this.tasks.push(...tasks);
    this.process();
  }

  /**
   * 处理队列中的任务
   */
  private process(): void {
    if (this.isProcessing || this.tasks.length === 0) {
      return;
    }

    this.isProcessing = true;
    this.currentHandle = requestIdleCallback(
      (deadline) => {
        // 在空闲时间内尽可能多地处理任务
        while (deadline.timeRemaining() > 0 && this.tasks.length > 0) {
          const task = this.tasks.shift();
          if (task) {
            try {
              task();
            } catch (error) {
              console.error('IdleTaskQueue task error:', error);
            }
          }
        }

        // 如果还有任务未处理,继续调度
        if (this.tasks.length > 0) {
          this.isProcessing = false;
          this.process();
        } else {
          this.isProcessing = false;
        }
      },
      { timeout: 5000 } // 5 秒超时,确保任务最终会被执行
    );
  }

  /**
   * 清空队列
   */
  clear(): void {
    this.tasks = [];
    if (this.currentHandle !== null) {
      cancelIdleCallback(this.currentHandle);
      this.currentHandle = null;
    }
    this.isProcessing = false;
  }

  /**
   * 获取队列中剩余任务数
   */
  get length(): number {
    return this.tasks.length;
  }
}

2. DOM 更新优化

优化前:

typescript 复制代码
// 使用 setTimeout 延迟更新
this.timmer = setTimeout(() => {
  patch(this.vNode, vNode);
  // ...
}, DOM_UPDATE_DELAY_MS);

优化后:

typescript 复制代码
// 使用 requestIdleCallback 在浏览器空闲时更新
this.idleCallbackHandle = requestIdleCallback(
  (deadline) => {
    // 检查是否有足够的时间
    if (deadline.timeRemaining() < 5 && !deadline.didTimeout) {
      // 重新调度到下一个空闲周期
      this.idleCallbackHandle = requestIdleCallback(/* ... */);
      return;
    }
    this.performDomUpdate(deadline);
  },
  { timeout: DOM_UPDATE_DELAY_MS }
);

优势:

  • 🚀 不阻塞主线程: DOM 更新在浏览器空闲时执行
  • 🎯 智能调度: 如果当前空闲时间不够,自动延迟到下一个空闲周期
  • ⏱️ 超时保护: 即使浏览器一直忙碌,也会在超时后执行

3. 批量更新优化

优化前:

typescript 复制代码
// 场景:数据变化时,更新对应的dom显示
handleHeartbeat = (uploader: FileUploader): void => {
  for (let i = 0; i < uploader.uploadingTaskList.length; i++) {
    const taskItem = uploader.uploadingTaskList[i];
    const item = this.itemsMap.get(taskItem.id);
    if (item) {
      item.updateDom(taskItem); // 立即执行
    }
  }
};

优化后:

typescript 复制代码
// 批量收集更新任务,在空闲时执行
handleHeartbeat = (uploader: FileUploader): void => {
  const updateTasks: Array<() => void> = [];

  for (let i = 0; i < uploader.uploadingTaskList.length; i++) {
    const taskItem = uploader.uploadingTaskList[i];
    const item = this.itemsMap.get(taskItem.id);
    if (item) {
      updateTasks.push(() => {
        item.updateDom(taskItem);
      });
    }
  }

  // 批量添加到空闲任务队列
  if (updateTasks.length > 0) {
    this.idleUpdateQueue.addBatch(updateTasks);
  }
};

优势:

  • 📦 批量处理: 多个 DOM 更新在同一个空闲周期内批量处理
  • ⚡ 减少重排: 减少浏览器重排/重绘次数
  • 🎯 优先级管理: 非关键更新不会阻塞关键操作

使用场景

适合使用 requestIdleCallback​ 的场景

  1. ✅ DOM 更新(非关键)

    • 进度条更新
    • 状态显示更新
    • 统计数据展示
  2. ✅ 数据统计/分析

    • 上传速度计算
    • 进度统计
    • 性能指标收集
  3. ✅ 预加载/预取

    • 预加载下一个文件
    • 预计算 MD5

不适合使用 requestIdleCallback​ 的场景

  1. ❌ 用户交互响应

    • 点击事件处理
    • 输入事件处理
    • 必须立即响应的操作
  2. ❌ 关键路径操作

    • 文件上传请求
    • 错误处理
    • 状态变更通知

性能收益

预期改进

  1. 主线程阻塞减少: DOM 更新不再阻塞主线程,提升页面响应性
  2. 帧率提升: 减少不必要的重排/重绘,提升动画流畅度
  3. CPU 使用优化: 在浏览器空闲时执行任务,更好地利用 CPU 资源
  4. 用户体验提升: 页面更流畅,交互更及时

实际测试建议

  1. Chrome DevTools Performance: 检查主线程阻塞情况
  2. FPS 监控: 监控帧率变化
  3. Lighthouse: 运行性能测试
  4. 真实场景测试: 测试大量文件上传时的性能

浏览器兼容性

原生支持

  • ✅ Chrome 47+
  • ✅ Edge 79+
  • ✅ Firefox 55+
  • ✅ Safari 不支持(需要降级方案)

降级方案

我们的实现自动提供了降级方案:

  • 使用 MessageChannel + setTimeout 模拟 requestIdleCallback
  • 确保所有浏览器都能正常工作
  • 性能可能略低于原生实现,但仍然比直接执行更好

最佳实践

1. 合理设置超时时间

typescript 复制代码
// 对于关键更新,设置较短的超时时间
requestIdleCallback(callback, { timeout: 100 });

// 对于非关键更新,可以设置较长的超时时间
requestIdleCallback(callback, { timeout: 5000 });

2. 检查空闲时间

typescript 复制代码
requestIdleCallback((deadline) => {
  // 如果时间不够,延迟执行
  if (deadline.timeRemaining() < 5 && !deadline.didTimeout) {
    // 重新调度
    return;
  }

  // 执行任务
  performTask();
});

3. 批量处理任务

typescript 复制代码
// 使用 IdleTaskQueue 批量处理
const queue = new IdleTaskQueue();
queue.addBatch([
  () => updateTask1(),
  () => updateTask2(),
  () => updateTask3(),
]);

4. 清理资源

typescript 复制代码
// 组件销毁时清理
destroy() {
  if (this.idleCallbackHandle) {
    cancelIdleCallback(this.idleCallbackHandle);
  }
  this.idleUpdateQueue.clear();
}

注意事项

  1. 超时时间: 设置合理的超时时间,确保任务最终会被执行
  2. 任务大小: 避免在单个空闲周期内执行过大的任务
  3. 错误处理: 确保任务中的错误不会影响后续任务
  4. 内存管理: 及时清理不再需要的回调句柄

未来优化方向

  1. IntersectionObserver: 结合使用,只更新可见区域的任务
  2. Web Workers: 将计算密集型任务移到 Worker 线程
  3. 虚拟滚动: 对于大量任务列表,使用虚拟滚动优化
  4. 增量更新: 只更新变化的部分,而不是整个 DOM
相关推荐
AAA阿giao1 小时前
深入解析 OOP 考题之 EditInPlace 类:从零开始掌握面向对象编程实战
前端·javascript·dom
西西学代码1 小时前
flutter---进度条(2)
前端·javascript·flutter
Apeng_09191 小时前
vue+canvas实现按下鼠标绘制箭头
前端·javascript·vue.js
wordbaby1 小时前
组件与外部世界的桥梁:一文读懂 useEffect 的核心机制
前端·react.js
wordbaby1 小时前
永远不要欺骗 React:详解 useEffect 依赖规则与“闭包陷阱”
前端·react.js
火星数据-Tina1 小时前
体彩数据API
前端·websocket
源码方舟1 小时前
【华为云DevUI开发实战】
前端·vue.js·华为云
VOLUN1 小时前
封装通用可视化大屏布局组件:Vue3打造高复用性的 ChartFlex/ChartFlexItem
前端·vue.js
bug总结1 小时前
“RTMP 怎么在 Web 端最简单、最省事地播放?
前端