Back

requestAnimationFrame vs setTimeout: 使い分けのガイドライン

requestAnimationFrame vs setTimeout: 使い分けのガイドライン

JavaScriptでスムーズなアニメーションを構築したり、タスクをスケジュールする際、requestAnimationFramesetTimeoutのどちらを選択するかは、アプリケーションのパフォーマンスに大きな影響を与えます。両方のメソッドとも関数の実行をスケジュールしますが、根本的に異なる目的を持ち、異なるタイミングメカニズムで動作します。

重要なポイント

  • 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のコールバックは、ブラウザがレイアウトを計算し、ピクセルを画面に描画する直前に実行されます。

この同期により、一般的なアニメーションの問題が解消されます:

  • 画面の破綻(Screen tearing): フレーム途中での更新による視覚的なアーティファクト
  • ジャンク(Jank): 不規則なフレームタイミングによるぎくしゃくした動き
  • 無駄なレンダリング: 表示されることのないフレームの描画

リソース効率

requestAnimationFrameは、ブラウザタブが非アクティブになると自動的に一時停止し、CPUサイクルとバッテリー寿命を節約します。setTimeoutはバックグラウンドタブでも実行を続けますが、ブラウザは約1分後に1秒に1回まで制限する場合があります。

// バッテリー効率の良いアニメーションループ
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 Workerの使用、またはアニメーションの複雑さの軽減を検討してください。

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