【JS応用】毎秒を出力する

毎秒を出力するという命題:高精度な時間制御とフロントエンドの課題

フロントエンド開発において「毎秒(1秒ごと)に何かを出力する」というタスクは、一見すると非常に単純な要件に見えます。`setInterval` を使えば実装できる、というのが多くのエンジニアの直感でしょう。しかし、ブラウザという環境は非決定的な要素が多く、厳密なタイミング制御には多くの落とし穴が存在します。本稿では、なぜ「毎秒出力」が難しいのか、そしてプロフェッショナルとしてどのような実装アプローチをとるべきかを徹底的に解説します。

なぜ setInterval は毎秒の出力に適さないのか

多くの初心者が最初に選択する `setInterval(callback, 1000)` は、実は「指定した間隔で実行されること」を保証していません。

第一に、JavaScriptはシングルスレッドで動作する言語です。メインスレッドが重い計算処理やDOM操作でブロックされている場合、`setInterval` のコールバックはキューに溜まり、実行が遅延します。これは、1000ミリ秒後に実行されるはずの処理が、1050ミリ秒や1100ミリ秒後に実行されることを意味します。

第二に、ブラウザの省電力機能です。多くの現代ブラウザは、タブが非アクティブになった場合やバッテリー駆動時に、タイマーの精度を意図的に落とします(1秒間隔のタイマーが数秒おきにしか発火しなくなることがあります)。また、`setInterval` は「前回の終了から何ミリ秒経過したか」ではなく「タイマーを開始してから何ミリ秒経過したか」を厳密に管理するわけではないため、累積的な誤差が発生しやすいという構造的な欠陥があります。

高精度な時間制御のための設計思想

「毎秒出力」を実現するために、私たちは以下の3つのアプローチを検討する必要があります。

1. 再帰的な setTimeout による制御
2. requestAnimationFrame を利用したフレーム同期
3. Web Workers を活用したメインスレッドからの分離

再帰的な `setTimeout` は、前回の処理が終わった後に次のタイマーをセットするため、処理時間が実行間隔に影響を与えにくいという利点があります。一方、`requestAnimationFrame` は画面の描画更新(通常60fps)に同期するため、UIの更新を伴う「毎秒出力」には最適です。しかし、厳密な時刻管理が必要な場合は、これらを組み合わせるか、時刻の差分を計算して補正するロジックが必要となります。

サンプルコード:高精度タイマーの実装

以下に、累積誤差を最小限に抑えつつ、正確に「毎秒」を実行するための実装例を示します。ここでは、`Date.now()` を用いて前回の実行時刻との差分を計測し、遅延が発生した場合には補正をかける手法を採用します。


class PreciseInterval {
  constructor(callback, interval = 1000) {
    this.callback = callback;
    this.interval = interval;
    this.expected = Date.now() + this.interval;
    this.timeout = null;
  }

  start() {
    this.timeout = setTimeout(this.tick.bind(this), this.interval);
  }

  stop() {
    clearTimeout(this.timeout);
  }

  tick() {
    const drift = Date.now() - this.expected;
    
    // 実行したことを通知
    this.callback();

    // 次の実行予定時間を計算(補正込み)
    this.expected += this.interval;
    
    // 次のタイムアウトをセット(遅延分を差し引く)
    const nextDelay = Math.max(0, this.interval - drift);
    this.timeout = setTimeout(this.tick.bind(this), nextDelay);
  }
}

// 使用例
const timer = new PreciseInterval(() => {
  console.log(`出力時刻: ${new Date().toISOString()}`);
}, 1000);

timer.start();

このコードの肝は、`drift`(遅延)の計算にあります。前回の予定時刻と現在の時刻の差分を計算し、次の `setTimeout` の待ち時間を調整することで、長期的に見た「毎秒」の精度を維持します。

実務におけるアドバイスとベストプラクティス

実務でタイマー処理を扱う際、単にコードを動かすだけでなく、以下の観点を持つことが重要です。

まず、「なぜ毎秒出力が必要なのか」という目的を問い直してください。もしUIの更新が目的であれば、`requestAnimationFrame` を使用するのがデファクトスタンダードです。ブラウザの描画パイプラインと同期させることで、不要な再描画を防ぎ、CPU負荷を軽減できます。

次に、バックグラウンド実行の考慮です。Webアプリケーションがバックグラウンドに回った際、タイマーの動作はブラウザのポリシーに依存します。もしバックグラウンドでも厳密な計測が必要な場合は、`Web Workers` を利用することを強く推奨します。Worker内であれば、メインスレッドのUIブロッキングの影響を受けず、タイマー処理を独立して実行できるため、非常に堅牢な設計になります。

また、メモリリークの管理も忘れてはなりません。タイマーをセットしたままコンポーネントがアンマウントされたり、ページ遷移が行われたりすると、不要なプロセスが残り続けます。`useEffect` のクリーンアップ関数や、クラスの `componentWillUnmount` で必ず `clearTimeout` を呼び出すことを徹底してください。

最後に、パフォーマンスの観点です。1秒ごとの処理が重い場合、その処理自体を別のスレッド(Worker)に逃がすか、あるいは処理内容を最適化して実行時間を16ms以下(60fpsのフレーム予算)に抑える必要があります。

まとめ

「毎秒を出力する」という要件は、フロントエンド開発における「非同期処理」と「ブラウザの実行モデル」を理解する絶好の機会です。`setInterval` を無批判に使うのではなく、その裏側にあるスレッドモデル、イベントループ、そしてブラウザの省電力ポリシーまでを考慮した設計が求められます。

高精度が求められるアプリケーションでは、累積誤差を補正する仕組みや、メインスレッドから独立した実行環境の検討が不可欠です。この記事で紹介した `PreciseInterval` のようなアプローチをベースにしつつ、要件に応じて `requestAnimationFrame` や `Web Workers` を使い分けることで、より堅牢でプロフェッショナルなフロントエンドアプリケーションを構築できるようになるでしょう。

技術選定において「動くこと」は最低条件であり、エンジニアの真価は「いかに制御可能で、予測可能で、効率的な実装を行うか」という点に現れます。この小さな「毎秒出力」というタスクに真摯に向き合うことが、複雑なフロントエンドシステムを構築する力へと繋がります。

コメント

タイトルとURLをコピーしました