Back

requestAnimationFrame vs setTimeout:何时使用哪个方法

requestAnimationFrame vs setTimeout:何时使用哪个方法

在构建流畅动画或在 JavaScript 中调度任务时,选择 requestAnimationFrame 还是 setTimeout 会显著影响应用程序的性能。虽然这两种方法都用于调度函数执行,但它们的根本用途不同,运行的时序机制也不同。

核心要点

  • requestAnimationFrame 与浏览器的显示刷新率同步,实现流畅的视觉更新
  • setTimeout 为非视觉任务和后台操作提供通用计时功能
  • 使用错误的方法可能导致性能问题、电池耗电和糟糕的用户体验
  • requestAnimationFrame 在非活动标签页中自动暂停,而 setTimeout 继续运行

理解核心差异

setTimeout:通用计时器

setTimeout 在指定的毫秒延迟后执行函数。它是一个简单、可预测的计时器,独立于浏览器的渲染周期工作。

// 1秒后执行
setTimeout(() => {
  console.log('One second has passed');
}, 1000);

// 带参数
setTimeout((message) => {
  console.log(message);
}, 2000, 'Hello after 2 seconds');

setTimeout 的关键特征是基于任务队列的执行。当延迟到期时,回调函数加入 JavaScript 事件循环的任务队列,与其他任务竞争执行时间。

requestAnimationFrame:动画专家

requestAnimationFrame(rAF)与浏览器的重绘周期同步,在大多数显示器上通常以每秒 60 帧的速度运行。它专门为视觉更新而设计。

function animate(timestamp) {
  // 基于时间戳更新动画
  const element = document.getElementById('animated-element');
  element.style.transform = `translateX(${timestamp / 10}px)`;
  
  // 继续动画
  if (timestamp < 5000) {
    requestAnimationFrame(animate);
  }
}

requestAnimationFrame(animate);

性能和时序考虑

浏览器渲染管道集成

requestAnimationFrame 的根本优势在于它与浏览器渲染管道的集成。虽然 setTimeout 在延迟到期时就会触发,但 requestAnimationFrame 回调在浏览器计算布局并将像素绘制到屏幕之前执行。

这种同步消除了常见的动画问题:

  • 屏幕撕裂:帧中更新造成的视觉伪影
  • 卡顿:不规则的帧时序导致运动断续
  • 无效渲染:绘制从未显示的帧

资源效率

requestAnimationFrame 在浏览器标签页变为非活动状态时自动暂停,节省 CPU 周期和电池寿命。setTimeout 在后台标签页中继续执行,尽管浏览器可能在大约一分钟后将其节流至每秒一次。

// 节能的动画循环
function gameLoop(timestamp) {
  updatePhysics(timestamp);
  renderGraphics();
  requestAnimationFrame(gameLoop);
}

// 使用 setTimeout 的低效方法
function inefficientLoop() {
  updatePhysics();
  renderGraphics();
  setTimeout(inefficientLoop, 16); // 尝试达到 60fps
}

实际使用场景

何时使用 requestAnimationFrame

对任何视觉更新使用 requestAnimationFrame

  • CSS 属性动画
  • Canvas 绘制操作
  • WebGL 渲染
  • DOM 位置更新
  • 动画期间的进度指示器
// 平滑滚动实现
function smoothScrollTo(targetY, duration) {
  const startY = window.scrollY;
  const distance = targetY - startY;
  const startTime = performance.now();
  
  function scroll(currentTime) {
    const elapsed = currentTime - startTime;
    const progress = Math.min(elapsed / duration, 1);
    
    // 缓动函数实现更平滑的运动
    const easeInOutQuad = progress * (2 - progress);
    
    window.scrollTo(0, startY + distance * easeInOutQuad);
    
    if (progress < 1) {
      requestAnimationFrame(scroll);
    }
  }
  
  requestAnimationFrame(scroll);
}

何时使用 setTimeout

对非视觉任务选择 setTimeout

  • 延迟 API 调用
  • 防抖用户输入
  • 轮询操作
  • 计划的后台任务
  • 一次性延迟
// 防抖搜索
let searchTimeout;
function handleSearchInput(query) {
  clearTimeout(searchTimeout);
  searchTimeout = setTimeout(() => {
    performSearch(query);
  }, 300);
}

// 带指数退避的重试逻辑
function fetchWithRetry(url, attempts = 3, delay = 1000) {
  return fetch(url).catch(error => {
    if (attempts > 1) {
      return new Promise(resolve => {
        setTimeout(() => {
          resolve(fetchWithRetry(url, attempts - 1, delay * 2));
        }, delay);
      });
    }
    throw error;
  });
}

常见陷阱和解决方案

帧率假设

永远不要假设 requestAnimationFrame 有固定的帧率。不同的显示器以不同的速率刷新(60Hz、120Hz、144Hz)。始终使用时间戳参数进行基于时间的动画:

let lastTime = 0;
function animate(currentTime) {
  const deltaTime = currentTime - lastTime;
  lastTime = currentTime;
  
  const element = document.getElementById('moving-element');
  const currentLeft = parseFloat(element.style.left) || 0;
  
  // 无论帧率如何,每秒移动 100 像素
  const pixelsPerMs = 100 / 1000;
  element.style.left = `${currentLeft + pixelsPerMs * deltaTime}px`;
  
  requestAnimationFrame(animate);
}

内存泄漏

组件卸载时始终存储并清除动画帧 ID:

let animationId;

function startAnimation() {
  function animate() {
    // 动画逻辑
    animationId = requestAnimationFrame(animate);
  }
  animationId = requestAnimationFrame(animate);
}

function stopAnimation() {
  if (animationId) {
    cancelAnimationFrame(animationId);
    animationId = null;
  }
}

// 页面卸载时清理
window.addEventListener('beforeunload', stopAnimation);

快速决策指南

使用场景最佳选择原因
流畅动画requestAnimationFrame与显示刷新同步
Canvas/WebGL 渲染requestAnimationFrame防止撕裂和卡顿
API 轮询setTimeout不依赖视觉更新
用户输入防抖setTimeout需要精确的延迟控制
动画期间的进度条requestAnimationFrame视觉反馈要求
后台数据处理setTimeout标签页非活动时继续执行
游戏循环requestAnimationFrame最佳性能和电池寿命

结论

requestAnimationFramesetTimeout 之间选择不是关于哪个”更好”——而是为工作选择正确的工具。对于视觉更新和动画,requestAnimationFrame 通过浏览器同步提供卓越的性能。对于一般计时需求和后台任务,setTimeout 提供您需要的灵活性和可预测性。理解这些差异确保您的 JavaScript 应用程序在高效管理系统资源的同时提供流畅的用户体验。

常见问题

虽然您可以使用 16.67ms 延迟的 setTimeout 来近似 60fps,但它不会与浏览器的实际刷新周期同步。这会导致丢帧、卡顿和浪费 CPU 周期。requestAnimationFrame 会自动适应显示器的刷新率。

是的,requestAnimationFrame 会适应显示器的刷新率。在 120Hz 显示器上,它大约每秒触发 120 次。始终使用时间戳参数计算增量时间,以在不同显示器上保持一致的动画速度。

如果您的回调超出帧预算,浏览器将跳过帧以保持响应性。这会导致可见的卡顿。考虑优化您的代码,使用 Web Workers 进行重计算,或降低动画复杂度。

Gain control over your UX

See how users are using your site as if you were sitting next to them, learn and iterate faster with OpenReplay. — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay