React 18 渲染机制优化:解决浏览器卡顿的三种方案

发布于:2025-06-12 ⋅ 阅读:(22) ⋅ 点赞:(0)

前言

React 18 带来了革命性的并发渲染机制,让我们能够更好地处理大量计算任务而不阻塞用户界面。本文将通过一个经典的面试题来深入探讨几种解决浏览器卡顿的方案,并分析 React 18 的内部实现原理。

问题场景

假设我们有一个函数 runTask,需要处理大量耗时任务:

function runTask(tasks) {
  while (tasks.length > 0) {
    const task = tasks.shift();
    task();
  }
}

// 模拟耗时任务
function createSlowTask(id) {
  return function () {
    const start = performance.now();
    // 模拟 3ms 的计算任务
    while (performance.now() - start < 3) {
      // 忙等待
    }
    console.log(`Task ${id} completed`);
  };
}

// 创建 100 个任务
const tasks = Array.from({ length: 100 }, (_, i) => createSlowTask(i + 1));

让我们创建一个完整的 HTML 页面来演示这个问题:

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>任务执行优化 Demo</title>
    <style>
      body {
        font-family: Arial, sans-serif;
        max-width: 800px;
        margin: 0 auto;
        padding: 20px;
      }

      .animation-container {
        width: 400px;
        height: 50px;
        border: 1px solid #ccc;
        position: relative;
        margin: 20px 0;
      }

      .moving-ball {
        width: 20px;
        height: 20px;
        background-color: #007bff;
        border-radius: 50%;
        position: absolute;
        top: 15px;
        animation: moveBall 3s linear infinite;
      }

      @keyframes moveBall {
        0% {
          left: 0;
        }
        50% {
          left: 360px;
        }
        100% {
          left: 0;
        }
      }

      button {
        padding: 10px 20px;
        margin: 10px;
        font-size: 16px;
        cursor: pointer;
      }

      .status {
        margin: 10px 0;
        padding: 10px;
        background-color: #f8f9fa;
        border-radius: 4px;
      }
    </style>
  </head>
  <body>
    <h1>任务执行优化演示</h1>

    <div class="animation-container">
      <div class="moving-ball"></div>
    </div>

    <div class="status" id="status">准备就绪</div>

    <button onclick="runBlockingTask()">执行阻塞任务</button>
    <button onclick="runOptimizedTask1()">requestIdleCallback 优化</button>
    <button onclick="runOptimizedTask2()">requestAnimationFrame 优化</button>
    <button onclick="runOptimizedTask3()">MessageChannel 优化</button>

    <script>
      // 创建耗时任务
      function createSlowTask(id) {
        return function () {
          const start = performance.now();
          while (performance.now() - start < 3) {
            // 模拟计算密集型任务
          }
          console.log(`Task ${id} completed`);
        };
      }

      // 原始阻塞版本
      function runBlockingTask() {
        document.getElementById('status').textContent = '执行阻塞任务中...';
        const tasks = Array.from({ length: 100 }, (_, i) => createSlowTask(i + 1));

        setTimeout(() => {
          const start = performance.now();
          while (tasks.length > 0) {
            const task = tasks.shift();
            task();
          }
          const end = performance.now();
          document.getElementById('status').textContent = `阻塞任务完成,耗时: ${(end - start).toFixed(2)}ms`;
        }, 10);
      }
    </script>
  </body>
</html>

当你点击"执行阻塞任务"按钮时,会发现小球的动画明显卡顿,这就是我们要解决的问题。

方案一:使用 requestIdleCallback

requestIdleCallback 允许我们在浏览器空闲时执行任务,避免阻塞主线程:

function runTaskWithIdleCallback(tasks) {
  if (tasks.length === 0) {
    document.getElementById('status').textContent = 'requestIdleCallback 任务全部完成';
    return;
  }

  requestIdleCallback((deadline) => {
    document.getElementById('status').textContent = `requestIdleCallback 执行中,剩余 ${tasks.length} 个任务`;

    while (deadline.timeRemaining() > 0 && tasks.length > 0) {
      const task = tasks.shift();
      task();
    }

    if (tasks.length > 0) {
      runTaskWithIdleCallback(tasks); // 递归处理剩余任务
    } else {
      document.getElementById('status').textContent = 'requestIdleCallback 任务全部完成';
    }
  });
}

function runOptimizedTask1() {
  const tasks = Array.from({ length: 100 }, (_, i) => createSlowTask(i + 1));
  runTaskWithIdleCallback(tasks);
}

方案二:使用 requestAnimationFrame

requestAnimationFrame 与浏览器的刷新率同步,我们可以利用每一帧的时间来处理任务:

function runTaskWithRAF(tasks) {
  if (tasks.length === 0) {
    document.getElementById('status').textContent = 'requestAnimationFrame 任务全部完成';
    return;
  }

  requestAnimationFrame(() => {
    const start = performance.now();
    const frameTime = 16.6; // 60fps 下每帧约 16.6ms

    document.getElementById('status').textContent = `requestAnimationFrame 执行中,剩余 ${tasks.length} 个任务`;

    while (performance.now() - start < frameTime && tasks.length > 0) {
      const task = tasks.shift();
      task();
    }

    if (tasks.length > 0) {
      runTaskWithRAF(tasks); // 递归处理剩余任务
    } else {
      document.getElementById('status').textContent = 'requestAnimationFrame 任务全部完成';
    }
  });
}

function runOptimizedTask2() {
  const tasks = Array.from({ length: 100 }, (_, i) => createSlowTask(i + 1));
  runTaskWithRAF(tasks);
}

方案三:使用 MessageChannel 实现时间切片

这种方案类似于 React 18 的内部实现,使用 MessageChannel 来调度任务:

// 基于 MessageChannel 的调度器
class TaskScheduler {
  constructor() {
    this.taskQueue = [];
    this.isScheduled = false;
    this.frameInterval = 5; // 每个时间片 5ms
    this.startTime = 0;

    // 创建 MessageChannel
    this.channel = new MessageChannel();
    this.port = this.channel.port2;
    this.channel.port1.onmessage = this.performWork.bind(this);
  }

  scheduleTask(callback) {
    this.taskQueue.push(callback);

    if (!this.isScheduled) {
      this.isScheduled = true;
      this.requestHostCallback();
    }
  }

  requestHostCallback() {
    this.port.postMessage(null);
  }

  performWork() {
    this.startTime = performance.now();
    let hasMoreWork = this.workLoop();

    if (hasMoreWork) {
      this.requestHostCallback();
    } else {
      this.isScheduled = false;
      document.getElementById('status').textContent = 'MessageChannel 任务全部完成';
    }
  }

  workLoop() {
    while (this.taskQueue.length > 0) {
      if (this.shouldYield()) {
        return true; // 还有更多工作
      }

      const task = this.taskQueue.shift();
      task();
    }

    return false; // 工作完成
  }

  shouldYield() {
    return performance.now() - this.startTime >= this.frameInterval;
  }

  updateStatus() {
    document.getElementById('status').textContent = `MessageChannel 执行中,剩余 ${this.taskQueue.length} 个任务`;
  }
}

// 使用调度器
const scheduler = new TaskScheduler();

function runTaskWithScheduler(tasks) {
  document.getElementById('status').textContent = '开始 MessageChannel 调度';

  tasks.forEach((task) => {
    scheduler.scheduleTask(() => {
      task();
      scheduler.updateStatus();
    });
  });
}

function runOptimizedTask3() {
  const tasks = Array.from({ length: 100 }, (_, i) => createSlowTask(i + 1));
  runTaskWithScheduler(tasks);
}

React 18 的并发渲染机制

React 18 内部使用了类似的时间切片机制,结合优先级调度(Lane 模型)来实现并发渲染:

// React 风格的优先级调度演示
class ReactLikeScheduler {
  constructor() {
    this.taskQueue = [];
    this.isScheduled = false;
    this.currentPriority = 'normal';

    // 优先级定义
    this.priorities = {
      immediate: 1,
      normal: 5,
      low: 10,
    };

    this.setupMessageChannel();
  }

  setupMessageChannel() {
    this.channel = new MessageChannel();
    this.port = this.channel.port2;
    this.channel.port1.onmessage = () => {
      this.flushWork();
    };
  }

  scheduleCallback(priority, callback) {
    const newTask = {
      callback,
      priority: this.priorities[priority] || this.priorities.normal,
      id: Math.random(),
    };

    // 插入任务队列并排序
    this.taskQueue.push(newTask);
    this.taskQueue.sort((a, b) => a.priority - b.priority);

    if (!this.isScheduled) {
      this.isScheduled = true;
      this.port.postMessage(null);
    }
  }

  flushWork() {
    const start = performance.now();

    while (this.taskQueue.length > 0 && performance.now() - start < 5) {
      const task = this.taskQueue.shift();
      task.callback();
    }

    if (this.taskQueue.length > 0) {
      this.port.postMessage(null);
    } else {
      this.isScheduled = false;
      console.log('所有任务完成');
    }
  }
}

// 使用示例
const reactScheduler = new ReactLikeScheduler();

// 调度不同优先级的任务
reactScheduler.scheduleCallback('immediate', () => {
  console.log('高优先级任务执行');
});

reactScheduler.scheduleCallback('normal', () => {
  console.log('普通优先级任务执行');
});

reactScheduler.scheduleCallback('low', () => {
  console.log('低优先级任务执行');
});

完整的演示页面

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>React 18 渲染优化演示</title>
    <style>
      body {
        font-family: Arial, sans-serif;
        max-width: 1000px;
        margin: 0 auto;
        padding: 20px;
        line-height: 1.6;
      }

      .demo-section {
        margin: 30px 0;
        padding: 20px;
        border: 1px solid #ddd;
        border-radius: 8px;
      }

      .animation-container {
        width: 100%;
        height: 60px;
        border: 2px solid #007bff;
        position: relative;
        margin: 20px 0;
        background: linear-gradient(90deg, #f8f9fa 0%, #e9ecef 100%);
      }

      .moving-ball {
        width: 30px;
        height: 30px;
        background: linear-gradient(45deg, #007bff, #0056b3);
        border-radius: 50%;
        position: absolute;
        top: 15px;
        animation: moveBall 4s ease-in-out infinite;
        box-shadow: 0 2px 8px rgba(0, 123, 255, 0.3);
      }

      @keyframes moveBall {
        0%,
        100% {
          left: 10px;
        }
        50% {
          left: calc(100% - 40px);
        }
      }

      .controls {
        display: flex;
        flex-wrap: wrap;
        gap: 10px;
        margin: 20px 0;
      }

      button {
        padding: 12px 24px;
        font-size: 14px;
        cursor: pointer;
        border: none;
        border-radius: 6px;
        background: #007bff;
        color: white;
        transition: all 0.2s;
      }

      button:hover {
        background: #0056b3;
        transform: translateY(-1px);
      }

      .status {
        margin: 15px 0;
        padding: 15px;
        background: #f8f9fa;
        border-left: 4px solid #007bff;
        border-radius: 4px;
        font-family: monospace;
      }

      .performance-info {
        display: grid;
        grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
        gap: 15px;
        margin: 20px 0;
      }

      .metric {
        padding: 15px;
        background: #e9ecef;
        border-radius: 6px;
        text-align: center;
      }

      .metric-value {
        font-size: 24px;
        font-weight: bold;
        color: #007bff;
      }
    </style>
  </head>
  <body>
    <h1>React 18 渲染优化技术演示</h1>

    <div class="demo-section">
      <h2>动画性能监测</h2>
      <p>观察小球动画的流畅度,点击不同按钮体验各种优化方案的效果。</p>

      <div class="animation-container">
        <div class="moving-ball"></div>
      </div>

      <div class="status" id="status">准备就绪 - 选择一个任务执行方案</div>

      <div class="performance-info">
        <div class="metric">
          <div class="metric-value" id="completedTasks">0</div>
          <div>已完成任务</div>
        </div>
        <div class="metric">
          <div class="metric-value" id="executionTime">0</div>
          <div>执行时间 (ms)</div>
        </div>
        <div class="metric">
          <div class="metric-value" id="fps">60</div>
          <div>当前 FPS</div>
        </div>
      </div>

      <div class="controls">
        <button onclick="runBlockingTask()">阻塞式执行</button>
        <button onclick="runOptimizedTask1()">requestIdleCallback</button>
        <button onclick="runOptimizedTask2()">requestAnimationFrame</button>
        <button onclick="runOptimizedTask3()">MessageChannel 时间切片</button>
        <button onclick="clearMetrics()">重置计数器</button>
      </div>
    </div>

    <script>
      // 性能监测
      let completedTaskCount = 0;
      let lastFrameTime = performance.now();
      let frameCount = 0;

      // FPS 监测
      function updateFPS() {
        frameCount++;
        const now = performance.now();
        if (now - lastFrameTime >= 1000) {
          document.getElementById('fps').textContent = frameCount;
          frameCount = 0;
          lastFrameTime = now;
        }
        requestAnimationFrame(updateFPS);
      }
      updateFPS();

      function updateMetrics() {
        document.getElementById('completedTasks').textContent = completedTaskCount;
      }

      function clearMetrics() {
        completedTaskCount = 0;
        updateMetrics();
        document.getElementById('executionTime').textContent = '0';
        document.getElementById('status').textContent = '计数器已重置';
      }

      // 创建耗时任务
      function createSlowTask(id) {
        return function () {
          const start = performance.now();
          while (performance.now() - start < 2) {
            // 模拟计算密集型任务
          }
          completedTaskCount++;
          updateMetrics();
        };
      }

      // 方案1:阻塞式执行
      function runBlockingTask() {
        document.getElementById('status').textContent = '⚠️ 执行阻塞任务中... (观察动画卡顿)';
        const tasks = Array.from({ length: 150 }, (_, i) => createSlowTask(i + 1));

        setTimeout(() => {
          const start = performance.now();
          while (tasks.length > 0) {
            const task = tasks.shift();
            task();
          }
          const end = performance.now();
          document.getElementById('executionTime').textContent = (end - start).toFixed(2);
          document.getElementById('status').textContent = `✅ 阻塞任务完成,总耗时: ${(end - start).toFixed(
            2
          )}ms (注意动画卡顿)`;
        }, 100);
      }

      // 方案2:requestIdleCallback
      function runTaskWithIdleCallback(tasks, startTime) {
        if (tasks.length === 0) {
          const totalTime = performance.now() - startTime;
          document.getElementById('executionTime').textContent = totalTime.toFixed(2);
          document.getElementById('status').textContent = `✅ requestIdleCallback 任务完成,总耗时: ${totalTime.toFixed(
            2
          )}ms`;
          return;
        }

        requestIdleCallback((deadline) => {
          document.getElementById('status').textContent = `🔄 requestIdleCallback 执行中,剩余 ${tasks.length} 个任务`;

          while (deadline.timeRemaining() > 1 && tasks.length > 0) {
            const task = tasks.shift();
            task();
          }

          runTaskWithIdleCallback(tasks, startTime);
        });
      }

      function runOptimizedTask1() {
        const tasks = Array.from({ length: 150 }, (_, i) => createSlowTask(i + 1));
        const startTime = performance.now();
        runTaskWithIdleCallback(tasks, startTime);
      }

      // 方案3:requestAnimationFrame
      function runTaskWithRAF(tasks, startTime) {
        if (tasks.length === 0) {
          const totalTime = performance.now() - startTime;
          document.getElementById('executionTime').textContent = totalTime.toFixed(2);
          document.getElementById(
            'status'
          ).textContent = `✅ requestAnimationFrame 任务完成,总耗时: ${totalTime.toFixed(2)}ms`;
          return;
        }

        requestAnimationFrame(() => {
          const frameStart = performance.now();
          const frameTime = 14; // 留出时间给渲染

          document.getElementById(
            'status'
          ).textContent = `🔄 requestAnimationFrame 执行中,剩余 ${tasks.length} 个任务`;

          while (performance.now() - frameStart < frameTime && tasks.length > 0) {
            const task = tasks.shift();
            task();
          }

          runTaskWithRAF(tasks, startTime);
        });
      }

      function runOptimizedTask2() {
        const tasks = Array.from({ length: 150 }, (_, i) => createSlowTask(i + 1));
        const startTime = performance.now();
        runTaskWithRAF(tasks, startTime);
      }

      // 方案4:MessageChannel 时间切片
      class TaskScheduler {
        constructor() {
          this.taskQueue = [];
          this.isScheduled = false;
          this.frameInterval = 4;
          this.startTime = 0;
          this.taskStartTime = 0;

          this.channel = new MessageChannel();
          this.port = this.channel.port2;
          this.channel.port1.onmessage = this.performWork.bind(this);
        }

        scheduleTask(callback) {
          this.taskQueue.push(callback);

          if (!this.isScheduled) {
            this.isScheduled = true;
            this.requestHostCallback();
          }
        }

        requestHostCallback() {
          this.port.postMessage(null);
        }

        performWork() {
          this.startTime = performance.now();
          let hasMoreWork = this.workLoop();

          if (hasMoreWork) {
            this.requestHostCallback();
          } else {
            this.isScheduled = false;
            const totalTime = performance.now() - this.taskStartTime;
            document.getElementById('executionTime').textContent = totalTime.toFixed(2);
            document.getElementById('status').textContent = `✅ MessageChannel 任务完成,总耗时: ${totalTime.toFixed(
              2
            )}ms`;
          }
        }

        workLoop() {
          while (this.taskQueue.length > 0) {
            if (this.shouldYield()) {
              document.getElementById(
                'status'
              ).textContent = `🔄 MessageChannel 执行中,剩余 ${this.taskQueue.length} 个任务`;
              return true;
            }

            const task = this.taskQueue.shift();
            task();
          }

          return false;
        }

        shouldYield() {
          return performance.now() - this.startTime >= this.frameInterval;
        }

        runTasks(tasks) {
          this.taskStartTime = performance.now();
          tasks.forEach((task) => this.scheduleTask(task));
        }
      }

      const scheduler = new TaskScheduler();

      function runOptimizedTask3() {
        const tasks = Array.from({ length: 150 }, (_, i) => createSlowTask(i + 1));
        scheduler.runTasks(tasks);
      }
    </script>
  </body>
</html>

性能对比分析

方案 优点 缺点 适用场景
阻塞执行 实现简单,执行效率高 严重阻塞 UI,用户体验差 仅适用于少量任务
requestIdleCallback 充分利用空闲时间,不影响关键渲染 Safari 不支持,执行时机不确定 后台数据处理,非关键更新
requestAnimationFrame 与浏览器刷新率同步,兼容性好 需要手动控制执行时间 动画相关任务,视觉更新
MessageChannel 可预测的执行时机,类似 React 实现 实现复杂度高 复杂应用的任务调度

总结

React 18 的并发渲染机制通过时间切片和优先级调度,有效解决了大量计算任务导致的浏览器卡顿问题。主要原理包括:

  1. 时间切片:将连续的渲染过程分割成多个小的时间片段
  2. 优先级调度:使用 Lane 模型为不同任务分配优先级
  3. 可中断渲染:允许高优先级任务中断低优先级任务的执行

在实际开发中,我们可以根据具体场景选择合适的优化方案:

  • 对于动画和视觉更新,优先考虑 requestAnimationFrame
  • 对于后台数据处理,可以使用 requestIdleCallback(注意兼容性)
  • 对于复杂的任务调度,可以参考 React 的 MessageChannel 实现

通过合理运用这些技术,我们能够构建出既高性能又用户体验友好的 Web 应用。