JavaScript | Web API:タイマー・スケジューリング - タイマー精度の問題

JavaScript JavaScript
スポンサーリンク

「タイマー精度の問題」は「指定した時間どおりには動かないことがある」という話

まず一番大事なことを先に言います。
setTimeout(…, 1000) と書いても、きっちり 1000ms 後に実行されるとは限りません。

JavaScript のタイマーは、

ブラウザの都合
CPU の負荷
タブがバックグラウンドかどうか
OS の制限

など、いろいろな要因の影響を受けます。

だから、「タイマーはだいたいこのくらいのタイミングで動くもの」 と考えるのが正解です。
ここを理解しておくと、「あれ?数値がズレてる…」というときに原因を見抜きやすくなります。


なぜ setTimeout / setInterval は「ぴったり」にならないのか

JavaScript は「1 本の線の上で順番に処理している」から

ブラウザの JavaScript は、基本的に シングルスレッド です。
つまり、「1 本の線の上で、1 つずつ順番に処理している」イメージです。

setTimeoutsetInterval は、

「◯ミリ秒後にこの関数を“実行キュー”に入れてね」

という予約をしているだけで、
「その瞬間に必ず実行する」と約束しているわけではありません。

もしそのタイミングで、
すでに別の重い処理が実行中だったら、

その処理が終わるまで待つ
→ 終わってから、ようやくタイマーのコールバックが実行される

という流れになります。

結果として、

1000ms 後に実行したかった
→ 実際には 1200ms 後になった

みたいなズレが普通に起きます。

setInterval は「ズレが積み重なっていく」ことがある

setInterval は「◯ミリ秒ごとに実行」と書けますが、
中の処理が重いと、間隔がどんどん伸びていく ことがあります。

例えば、こういうコードを考えます。

setInterval(() => {
  const start = Date.now();
  while (Date.now() - start < 150) {
    // 150ms かかる重い処理
  }
  console.log("tick");
}, 100);
JavaScript

100ms ごとに実行したいのに、
1 回の処理に 150ms かかっています。

結果として、

1 回目:150ms かかる
→ すでに 100ms を超えているので、次の実行は「すぐにキューに入る」
→ でもまた 150ms かかる

という感じで、
**「100ms ごと」どころか「150ms ごと以上」になってしまいます。

「指定した間隔どおりに動く」と思い込むと、ここでハマります。


「最小遅延時間」の制限(特にバックグラウンドタブ)

タブがバックグラウンドだと、タイマーは間引かれる

ブラウザは、
バックグラウンドタブのタイマーをわざと遅くする ことがあります。

理由はシンプルで、

見えていないタブでガンガンタイマーを回されると
CPU とバッテリーが無駄に消費されるから

です。

そのため、バックグラウンドタブでは、

setTimeout(fn, 10) と書いても、
実際には 1000ms ごとにしか動かない

といった「最小遅延時間」が強制されることがあります。

つまり、

「このタブがアクティブかどうか」で
タイマーの精度が変わる

ということです。

セキュリティ・プライバシーの観点からの制限

最近のブラウザは、
タイミング情報を使った攻撃(タイミング攻撃)や指紋取得 を防ぐために、
タイマーの精度をわざと落とすことがあります。

performance.now() などの高精度タイマーも、
環境によっては「少し丸められた値」が返ってきます。

これも、

「タイマーは環境によって精度が変わる」
「ミリ秒単位で完全に信用してはいけない」

という話につながります。


「時間を測りたいとき」にタイマーに頼りすぎてはいけない

setTimeout の回数 × 間隔 = 経過時間、ではない

例えば、「10 秒のカウントダウン」を作りたいとします。

こんなコードを書きたくなるかもしれません。

let count = 10;

const id = setInterval(() => {
  count--;
  console.log(count);

  if (count <= 0) {
    clearInterval(id);
    console.log("終了");
  }
}, 1000);
JavaScript

一見正しそうですが、
タイマーがズレていくと、

実際には 11 秒かかったり
9.8 秒くらいで終わったり

ということが起きます。

「回数 × 間隔 = 正確な時間」にはならない のがポイントです。

正確に時間を扱いたいときは「現在時刻から計算する」

時間をちゃんと扱いたいときは、
「今何時か」を毎回測って差分を計算する のが基本です。

const start = Date.now();

const id = setInterval(() => {
  const now = Date.now();
  const elapsed = Math.floor((now - start) / 1000); // 経過秒数

  const remaining = 10 - elapsed;
  console.log(remaining);

  if (remaining <= 0) {
    clearInterval(id);
    console.log("終了");
  }
}, 200);
JavaScript

ここでは 200ms ごとにチェックしていますが、
「経過時間は Date.now() から計算している」 ので、
タイマーのズレがあっても、
カウントダウン自体はほぼ 10 秒で終わります。

重要なのは、

タイマーは「きっかけ」をくれるだけ
正確な時間は「時計(Date.now や performance.now)」から取る

という役割分担です。


requestAnimationFrame と「時間ベースのアニメーション」

フレーム数ではなく「経過時間」で動かす

アニメーションでも同じ話が出てきます。

「60fps だから、1 フレームごとに 5px 動かせば 1 秒で 300px」
と考えると、
フレームレートが落ちたときに動きが遅くなります。

そこで、requestAnimationFrame のコールバックに渡される
「time(経過時間)」 を使います。

let start = null;

function animate(time) {
  if (!start) start = time;

  const progress = time - start; // 経過ミリ秒
  const x = (progress / 1000) * 300; // 1秒で300px 動く

  box.style.transform = `translateX(${Math.min(x, 300)}px)`;

  if (progress < 1000) {
    requestAnimationFrame(animate);
  }
}

requestAnimationFrame(animate);
JavaScript

ここでは、

フレーム数ではなく「経過時間」で位置を決めている
→ フレームレートが落ちても、1 秒で 300px 動く

という設計になっています。

これも、

「タイマーやフレーム数に依存せず、時間そのものを信じる」

という考え方の応用です。


「タイマー精度の問題」とどう付き合うべきか

タイマーは「ざっくりしたトリガー」として使う

まず前提として、

setTimeout / setInterval
「◯ミリ秒後に必ず実行」ではなく
「◯ミリ秒後以降のどこかで実行される」

くらいの感覚で捉えるのが健全です。

だから、

正確な時間計測
厳密なスケジューリング

には、タイマー単体を頼りすぎないことが大事です。

正確さが必要なときは「時計」と組み合わせる

カウントダウン
アニメーションの進行度
経過時間の表示

など、「時間」が意味を持つ場面では、

開始時刻を記録する
現在時刻との差分で「本当の経過時間」を計算する
タイマーは「更新のきっかけ」としてだけ使う

というパターンを基本にすると、
タイマー精度の問題に振り回されにくくなります。


初心者として「タイマー精度の問題」で本当に掴んでほしいこと

setTimeout / setInterval は「指定した時間ぴったり」ではなく、「それ以降のどこか」で実行される
重い処理が走っていると、タイマーはどんどん遅れる(特に setInterval はズレが積み重なる)
バックグラウンドタブやブラウザの制限で、最小遅延時間が強制的に長くなることがある
「回数 × 間隔 = 経過時間」とは限らないので、正確な時間は Date.now や performance.now から計算する
タイマーは「きっかけ」、時間の正確さは「時計」で担保する、という役割分担を意識する

もし試してみたくなったら、
setInterval で 100ms ごとに Date.now() の差分をログに出してみてください。
「思ったよりブレてるな」という感覚を一度体で知ると、
タイマーを使うときの設計が、ぐっと現実的で賢いものになります。

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