JOURNAL

スマホブラウザの『バックグラウンドタブ』はどれくらい制限されるのか

「ブラウザの裏に回したタブって、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プログラミングの面白さであり醍醐味だなと感じています。