主要ブラウザは表示していないタブのsetTimeout/setInterval間隔を間引くことが知られています。
この挙動について調査してみました。
実験的に確認する まずは挙動を実験的に確認してみます。この方法はいつでも簡単に実施できるので、未知のブラウザや新しいバージョンに対しても容易に結果を得ることができます。
次のような簡易的なページを作って実験的に確認してみます。このページではタイマーが発火した時刻を記録し、その間隔を10秒ごとにまとめて60秒間分の履歴を表示するようにしています。これにより、バックグラウンドから復帰したときに直前60秒分の大まかな遷移がわかります。
https://gist.github.com/qnighy/898b76c0fb3369e013814c66c5fabaad
フォアグランドでは以下のように、記載通りの値に収束します。
一方、タブをバックグラウンド化 (別のタブに移動) して1分ほど放置してから戻ってくると、このような表示になります。これは1秒に1回まで間引かれていることを意味しています。 (Mac上のGoogle Chromeの例)
Mac上の主要な3つのWebブラウザに関して、手元で確認した感じでは以下のような結果になりました。 (ただし、電源状態などのテスト条件によって結果が異なる可能性は考えられるので注意してください)
Google Chrome: 少なくとも数分間は1秒に1回の頻度になるように間引かれて実行される。ウインドウ全体を隠してからしばらくすると20秒に1回程度まで間引かれる。 Firefox: 少なくとも数分間は1秒に1回の頻度になるように間引かれて実行される。 Safari: はじめは1〜4秒ほどまで間引かれる。その後数秒すると、徐々に間隔が増えて20秒に1回程度まで間引かれる。少なくとも数分間はその頻度が維持される。 コードリーディング 今回は特に大きく間引かれていたSafariについて、その正確な挙動を確認してみます。
タイマー用の定数 として有名な "4ms" (フォアグラウンドタブで5回以上タイマーを繰り返したときのスロットリング挙動) などをヒントに関連するソースコードを探すと、DOMTimer.h に以下の定数が見つかります。
static Seconds defaultMinimumInterval() { return 4_ms; }
static Seconds defaultAlignmentInterval() { return 0_s; }
static Seconds defaultAlignmentIntervalInLowPowerMode() { return 30_ms; }
static Seconds nonInteractedCrossOriginFrameAlignmentInterval() { return 30_ms; }
static Seconds hiddenPageAlignmentInterval() { return 1_s; }
そこでこれらの利用箇所を辿っていくと以下のことがわかります。
この、フォアグラウンドタブにおけるアラインメント計算ルールは以下のようになっています。
タイマーイベントの段数が5段未満のときは 0ms のまま。 5段以上の場合は、以下の3つのうち最大のアラインメントが使われる。 バックグラウンドスロットリングが有効な場合は、 DOMTimer::hiddenPageAlignmentInterval の値 (1s) ページオブジェクトが存在する場合は、 page->domTimerAlignmentInterval() の値 不可視のクロスオリジンフレーム内のドキュメントである場合は、DOMTimer::nonInteractedCrossOriginFrameAlignmentInterval の値 (30ms) ここで、先ほどChromeで実験したときに観測された「1秒」という間隔の根拠はわかりました。
さらにスロットリングが「20秒」まで伸びる挙動を解明するには、 page->domTimerAlignmentInterval() を読む必要があります。 (これはScriptExecutionContextやDocumentのメソッドとは別です)
domTimerAlignmentInterval メソッド自体は m_domTimerAlignmentInterval を返す だけです。その m_domTimerAlignmentInterval は以下のように更新されます。
増分スロットリングでは、増分スロットリングが有効化されてからの経過時間をそのまま次のインターバルにしています。そのため、1回あたりおよそ2倍に増えているように見えることになります。
さて、この増分スロットリングの上限はWebCoreの呼び出し元から設定できるようになっています。この呼び出し元を辿ると、以下の設定に行き着きます。
// We're estimating an upper bound for a set of background timer fires for a page to be 200ms
// (including all timer fires, all paging-in, and any resulting GC). To ensure this does not
// result in more than 1% CPU load allow for one timer fire per 100x this duration.
static int maximumTimerThrottlePerPageInMS = 200 * 100;
つまり、20秒が上限となっているようです。
Chromeについては未確認ですが、元は同じコードベースであることも考えると、同様に実装されている可能性は高いでしょう。
どうするのが良いのか バックグラウンドタイマーが必要なのはポーリング目的のことが多いでしょう。以下のことをまず検討するのがよいでしょう。
1秒より短い間隔でポーリングする必要があるか? 20秒より短い間隔でポーリングする必要があるか? この場合、ポーリング間隔は指数的に減衰するので、待機時間が短ければ間引きによるロスも短くなることが期待されるので、20秒という数字ほど悪い結果にはならないとも考えられます。 もし、これらの間引かれたポーリング頻度では不十分な場合は、background workerを立ててポーリングする必要があるかもしれません。
(別の方法として、スロットリングされそうになったときにたくさんのタイマーを起動すればオフセットがいい感じに分散し、結果として十分な頻度でポーリングできる可能性はありますが、あまり良い方法ではなさそう)
まとめ setIntervalはバックグラウンドタブでは間引かれて実行される。 間引かれたときの間隔は実装によるが、標準的な値は「1秒に1回」である。そこから徐々にスロットリングを強化し「20秒に1回」程度まで遅くするブラウザもある。