「ブラウザの裏に回したタブって、JavaScriptの処理はどこまで動くんだろう?」というのは、タイマーや定期計測ツールを作っている開発者が必ず一度はぶち当たる深い沼です。赤猫座の道具(SPLIT TIMERなど)を作っている時も、この「ブラウザのバックグラウンド間引き(スロットリング)」との戦いには相当な時間を費やしました。
今回は、タイマーが狂う原因とブラウザの省電力仕様、そしてフロントエンド開発における正しい対処法について、泥臭い実体験を交えて詳しくまとめておきます。
1. setIntervalは容赦なく裏切る
タイマー機能を作るとき、誰もが最初に思いつくのがsetInterval(callback, 1000)で1秒ごとにカウントを減らしていく素朴な実装です。
// 初心者が必ずやって爆死するコード
let timeLeft = 180; // 3分タイマー
setInterval(() => {
timeLeft--;
updateDisplay(timeLeft);
}, 1000);
表でタブを表示している間は、これでも時計通りに1秒ずつチクタクと完璧に動きます。「よし完成!」と喜んでカップラーメンにお湯を注ぎ、別タブでYouTube動画を見ながら待つこと数分。戻ってきて画面を見たら……「残り時間:2分45秒」。裏切りの星…。
2. ブラウザが裏で仕掛けてくる「スロットリング(間引き)」の正体
なぜこんなことが起きるのかというと、現代のWebブラウザ(Chrome, Safari, Edgeなど)は、ユーザーのPCバッテリーとCPUを節約するために、「非アクティブな裏タブのタイマーを強制的に間引く仕様」を徹底的に組み込んでいるからです。
- PCブラウザの裏タブ:通常時は1ms単位で動くタイマーも、別タブへ移動すると最低でも1秒(1000ms)間隔に制限され、さらに数分間放置されると「1分(60000ms)に1回」まで容赦なく実行頻度が落とされます(ChromeのAggressive Throttlingなど)。
- スマホ(iOS Safari / Android):別アプリに切り替えたり画面がスリープした瞬間、JavaScriptの実行スレッドそのものがほぼ完全に一時停止(フリーズ)します。
つまり、「1秒ごとに実行されるはず」という前提で timeLeft-- しているプログラムは、裏に回った瞬間に時間の進みが数倍〜数十倍遅れるか、完全に時が止まってしまうのです。
3. 解決策:ループ回数を数えず「絶対時間」から逆算する
この問題に対する唯一にして最大の正解は、「何回動いたかを数えるのではなく、開始時刻(UNIXタイムスタンプ)との引き算で残り時間を都度計算する」というアプローチ(Delta Time方式)です。
// スロットリングに完全耐性を持つ実装
const duration = 180 * 1000; // 3分(ミリ秒)
const startTime = Date.now(); // 開始時刻を絶対値で記録
const endTime = startTime + duration;
function tick() {
const now = Date.now();
const remaining = Math.max(0, endTime - now); // 常に現在時刻との差分を計算
updateDisplay(Math.ceil(remaining / 1000));
if (remaining > 0) {
requestAnimationFrame(tick); // または setTimeout
} else {
onTimerComplete(); // タイマー終了
}
}
tick();
この書き方なら、裏タブで処理が何分間サボられようが、スマホがスリープしようが関係ありません。次にコードが息を吹き返した瞬間の Date.now() から正確な残り時間が即座に算出され、一瞬で正しい時計の針へと追いつきます。そう、今を生きるのです!
4. Page Visibility APIで「戻ってきた瞬間」を迎え撃つ
さらにユーザー体験を高めるなら、ブラウザの表示状態を検知できるPage Visibility APIとの連携が欠かせません。
裏タブから表タブに戻ってきた瞬間に visibilitychange イベントが発火するので、そこで画面の再描画や音・アニメーションの同期を一気に走らせます。
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
// ユーザーがタブに戻ってきた!
syncTimerDisplay(); // 画面のズレを一瞬で再計算・同期
} else {
// タブが裏に回ったときの軽量化処理など
}
});
5. 【小話】Web Workerという抜け道と、モバイルの壁
「メインスレッドが間引かれるなら、別スレッドのWeb WorkerでsetIntervalを回せば裏でも正確に動くんじゃないか?」
これは多くの開発者が一度は試みる有名なハックです。実際、PC版ブラウザであればWeb Worker内のタイマーはタブが非アクティブでも比較的高い精度で動き続けます。
……しかし、モバイル(特にiOS Safari)のOSレベルの省電力の前にはWeb Workerすら無力です。スマホが画面ロックに入ったり別アプリに遷移したりすると、WorkerスレッドごとOSにサスペンド(強制睡眠)されるため、結局は絶対時間による逆算設計が必須になります。
| 実装アプローチ | 裏タブでの精度 | モバイル画面ロック時 | 総合評価・おすすめ度 |
|---|---|---|---|
| setIntervalで1ずつ減算 | ×(大きく遅延・停止) | ×(完全に停止) | ✕ 絶対に使ってはいけない |
| Date.now()による絶対時間逆算 | ◎(復帰時に即座に一致) | ◎(復帰時に即座に一致) | ★ 最も堅牢で標準的な正解 |
| Web Workerでバックグラウンド実行 | ○(PCならほぼ狂わない) | ×(OSによりサスペンド) | △ PC特化の音再生等には有効 |
| Page Visibility APIとの併用 | ◎(復帰時のUI破綻を防止) | ◎(滑らかに再同期) | ★ 差分計算とセットで必須 |
結論:ブラウザの省電力機能と戦うな、受け入れろ
昔は「なんでブラウザは勝手に処理を止めるんだよ!」と理不尽に感じたこともありましたが、もし全タブが裏で全力でJavaScriptをぶん回していたら、PCのファンは爆音で回り、スマホのバッテリーは数時間で空っぽになってしまいます。
ブラウザのスロットリングは、ユーザーの端末を守るための高度な優しさです。その優しさと真正面から喧嘩するのではなく、「裏で止められても平気な顔をして復帰できる設計」をスマートに組むのが、Webプログラミングの面白さであり醍醐味だなと感じています。
