浏览器渲染流水线与事件循环

深入理解浏览器渲染与事件循环的交互、requestAnimationFrame 的作用、以及为什么动画应该用 RAF 而不是 setTimeout。


浏览器的渲染流水线

浏览器的页面渲染不是即时的,而是经过一系列步骤:

  1. JavaScript 执行:修改 DOM 或样式
  2. Style:计算每个元素的样式
  3. Layout:计算每个元素的几何信息(位置、大小)
  4. Paint:将元素绘制到多个图层
  5. Composite:将多个图层合成为最终页面

这些步骤并不是每次都完整执行。如果只是修改了颜色等不影响布局的属性,浏览器可以跳过 Layout 步骤。


渲染与事件循环的关系

浏览器在每次事件循环迭代中,会在特定时机检查是否需要渲染:

  1. 所有微任务执行完毕后
  2. requestAnimationFrame 回调执行
  3. 检查是否需要 Layout/Paint
  4. 如果需要渲染,在下一个宏任务前进行

渲染时机

console.log('1');

requestAnimationFrame(() => {
  console.log('2');
});

console.log('3');

// 输出: 1, 3, 2

requestAnimationFrame 的回调在渲染前被调用,而不是在微任务之后立即调用。


为什么 setTimeout 不适合动画

// 错误的动画实现
let position = 0;
function animate() {
  position += 5;
  element.style.transform = `translateX(${position}px)`;
  setTimeout(animate, 16);
}

问题:

  • setTimeout 只是把回调放进宏任务队列,不保证在哪个渲染时机执行
  • 如果回调在两帧之间执行,会导致跳帧
  • 页面不可见时(如切换标签页),动画仍然执行,浪费 CPU

requestAnimationFrame 的正确用法

function animate() {
  // 更新动画状态
  updateAnimation();

  // 浏览器会在下一帧渲染前调用
  requestAnimationFrame(animate);
}

requestAnimationFrame(animate);

requestAnimationFrame 的优势:

  1. 与浏览器渲染同步:回调在渲染前执行,保证每帧都能更新
  2. 页面不可见时自动暂停:切换标签页时,RAF 停止执行,节省资源
  3. 自动降频:如果设备无法达到 60fps,RAF 会自动减少调用次数

宏任务、微任务、渲染的完整时序

console.log('1');

setTimeout(() => {
  console.log('2');
}, 0);

Promise.resolve().then(() => {
  console.log('3');
});

requestAnimationFrame(() => {
  console.log('4');
});

console.log('5');

执行顺序:

  1. 同步代码:1, 5
  2. 微任务:3
  3. requestAnimationFrame 回调:4(在渲染前)
  4. setTimeout 回调:2(下一个宏任务)

长任务与渲染阻塞

如果 JavaScript 执行时间过长(> 50ms),会阻塞渲染,导致页面卡顿。这就是"长任务"问题。

// 长任务:阻塞渲染
function heavyComputation() {
  const result = calculateLargeDataset(); // 假设需要 100ms
  return result;
}

解决方案:使用 requestIdleCallback 将工作拆分到多个帧:

function performWork(deadline) {
  while (deadline.timeRemaining() > 0 && workQueue.length > 0) {
    processNextItem();
  }

  if (workQueue.length > 0) {
    requestIdleCallback(performWork);
  }
}

requestIdleCallback(performWork);

这一章想说的

浏览器渲染和事件循环紧密关联:

  • 渲染发生在微任务执行完毕后、requestAnimationFrame 回调之后
  • requestAnimationFrame 是动画的正确 API,与渲染同步,自动节流
  • setTimeout 不适合动画,因为它不保证渲染时机
  • 长任务会阻塞渲染,应该拆分到多个帧

延展阅读