【開発者向け】even-toolkit の keep-alive を発見!実はバックグラウンド対策は二重に効いていた
前回の記事では、G2スマートグラスアプリでタイマー表示をバックグラウンドでも止めないための5つのテクニックを紹介しました。
でも・・・実は前回の実装、自分でもちょっと不思議だったんです。
この記事では、この疑問と衝撃の事実を解説します!
自分でも不思議だった・・・
前回記事の方式は動いているとはいえフツーのJavaScriptアプリだとすると、ブラウザがバックグラウンドになると `setInterval` は大幅にスロットリングされるはず。タイムスタンプ方式で「残り時間の値」が正確なのは分かるけど、そもそも setInterval がちゃんと100msで回り続けていること自体がおかしい。表示の更新頻度がもっと落ちてもおかしくないのに、なぜかスムーズに動く。なんでだろう?
そんなモヤモヤを抱えていたところ、Even の Discord で「keep-alive」という言葉を見つけました。even-toolkit にそんなユーティリティがあるらしい。「これが使えるのか?」と思ってソースコードを読んでみたところ……
なんと、すでに使っていた。
`useGlasses` フックの中で、keep-alive は自動的に有効化されていたのです。つまり前回の実装がうまくいっていたのは、知らないうちに keep-alive が setInterval のスロットリングを防いでくれていたから。
そしてその上に、自分で書いたタイムスタンプ方式が「万が一 keep-alive が突破されても大丈夫」な安全策として機能していた。
意図せず多層防御が完成していたわけです。
以下、keep-alive の仕組みを解説し、前回のタイムスタンプ方式との関係を整理します。
keep-alive とは
even-toolkit が提供する、ブラウザのバックグラウンドスロットリングを防止するユーティリティです。
import { activateKeepAlive, deactivateKeepAlive } from 'even-toolkit/keep-alive';
activateKeepAlive('myapp_keep_alive'); // 有効化
deactivateKeepAlive(); // 無効化`useGlasses` フック内で自動的に呼ばれるため、通常は開発者が意識する必要はありません。
keep-alive の仕組み
2つのメカニズムを併用して、ブラウザに「このタブはまだアクティブに仕事をしている」と思わせます。
https://github.com/fabioglimb/even-toolkit/blob/main/glasses/keep-alive.ts
1. サイレントオーディオ(Web Audio API)
audioCtx = new AudioContext();
oscillator = audioCtx.createOscillator();
oscillator.frequency.value = 1; // 1Hz — 人間には聞こえない
const gain = audioCtx.createGain();
gain.gain.value = 0.001; // 音量0.1% — 聞こえない
oscillator.connect(gain);
gain.connect(audioCtx.destination);
oscillator.start();1Hzのオシレーターを音量0.1%で再生し続ける
人間には完全に聞こえない
ブラウザから見ると「オーディオを再生中のタブ」になる
Chromeはオーディオ再生中のタブのスロットリングを緩和する
2. Web Locks API
if (navigator.locks) {
navigator.locks.request(lockName, () => {
return new Promise<void>(() => {}); // 永遠にresolveしないPromise
});
}名前付きロックを取得し、永遠に保持し続ける
ロックを持っているタブは「アクティブな処理中」とみなされる
ブラウザがタブをサスペンド(完全停止)するのを防ぐ
useGlasses との統合
keep-alive は `useGlasses` フック内で自動管理されています。
// useGlasses 内部(even-toolkit)
activateKeepAlive(`${appName}_keep_alive`); // マウント時
// クリーンアップ
deactivateKeepAlive(); // アンマウント時つまり、`useGlasses` を使っていればkeep-alive は最初から有効です。
では、前回のテクニックは不要だったのか?
いいえ。 keep-alive は万能ではありません。
keep-alive が「できること」
アプローチ : スロットリングの予防
役割 : ブラウザに「まだ仕事中」と伝える
setInterval の精度 : 維持しようとする
保証の強さ : ベストエフォート(OSやブラウザ次第)
keep-alive が効かないケース
OS レベルの制限 — iOSのバックグラウンドアプリ制限、Android のバッテリー最適化など、ブラウザより上位の制約には対抗できない
ブラウザのポリシー変更 — Chrome は定期的にバックグラウンドタブの扱いを変更している。今日効く手法が来月も効く保証はない
低バッテリーモード — 端末がバッテリーセーバーモードだとブラウザの判断が変わることがある
長時間バックグラウンド — 30分以上のバックグラウンドでは、オーディオ再生していてもスロットリングが強化されるブラウザもある
前回記事のタイムスタンプ方式が「できること」
keep-alive が突破されても:
`phaseDeadline - Date.now()` は常に正しい残り時間を返す
`transitionIfExpired()` は次のtickで遅延分をすべて回収する
display関数の `getRemaining()` は描画の瞬間に正確な値を計算する
keep-alive は「setInterval を100msで動かし続けたい」。タイムスタンプ方式は「setInterval が10秒に1回しか動かなくても正しく動く」。
2つのアプローチの関係
ブラウザのバックグラウンド制限
│
┌─────────┴─────────┐
▼ ▼
keep-alive タイムスタンプ方式
(予防 / 攻め) (耐性 / 守り)
│ │
スロットリングを スロットリングされても
起きにくくする 正確に動くようにする
│ │
└─────────┬─────────┘
▼
両方あると最も安定これは、セキュリティでいう「多層防御(Defense in Depth)」と同じ考え方です。
第1層(keep-alive): そもそもスロットリングを防ぐ → 理想的なケースでは100msごとに安定して更新
第2層(タイムスタンプ方式): 第1層が突破されても正確性を保つ → 最悪のケースでも復帰時に即座に正しい状態に戻る
実際にどう効いているか
keep-alive あり + タイムスタンプ方式あり(現状)
バックグラウンド中:
setInterval(100ms) → 実際は ~100ms で実行 ← keep-aliveのおかげ
毎tick: phaseDeadline - Date.now() → 正確な残り時間
フォアグラウンド復帰:
ほぼ即座に最新表示(ズレが小さい)keep-alive あり + タイムスタンプ方式なし
バックグラウンド中:
setInterval(100ms) → 実際は ~100ms で実行
毎tick: timeRemaining -= 0.1 → ほぼ正確
keep-aliveが突破された場合:
setInterval が1秒〜数分に1回に
timeRemaining -= 0.1 が遅延 → タイマーが大幅にズレる ← 危険!keep-alive なし + タイムスタンプ方式あり
バックグラウンド中:
setInterval(100ms) → 実際は 1秒〜数分に1回
毎tick: phaseDeadline - Date.now() → それでも正確
フォアグラウンド復帰:
即座に正しい残り時間を表示
表示の更新頻度は落ちるが、表示される値は常に正確keep-alive なし + タイムスタンプ方式なし
バックグラウンド中:
setInterval が遅延 + 残り時間もズレる → 完全に壊れるまとめ
even-toolkit の `useGlasses` を使っていれば keep-alive は自動的に有効になります。その上で、前回の記事で紹介したタイムスタンプ方式を組み合わせることで、どんな環境でも止まらないグラスアプリが実現できそうです!
さいごに
もしよかったら「スキ!」いただけますと、嬉しいです!
