Chrome 88(2021 年 1 月)では、特定の条件下で非表示ページのチェーン化された JavaScript タイマーを大幅にスロットリングします。これにより CPU 使用率が下がり、バッテリー使用量も削減されます。この動作が変更されるエッジケースもありますが、タイマーは別の API のほうが効率的で信頼性が高い場合によく使用されます。
専門用語が多すぎて意味がわからなくなってしまいました。では、詳しく見ていきましょう。
用語
非表示のページ
通常、非表示とは、別のタブがアクティブになっているか、ウィンドウが最小化されているものの、そのコンテンツがまったく表示されないページがあるとブラウザによって認識されることを意味します。一部のブラウザでは他のブラウザよりも高度な処理が行われますが、Page Visibility API を使用すると、ブラウザが表示が変化したと認識したときにいつでもトラッキングできます。
JavaScript タイマー
JavaScript タイマーとは、setTimeout
と setInterval
のことで、後でコールバックをスケジュールできます。タイマーは有用で、なくなることはありませんが、イベントがより効率的で正確である場合に、状態をポーリングするために使用される場合もあります。
チェーン タイマー
setTimeout
コールバックと同じタスク内で setTimeout
を呼び出した場合、2 回目の呼び出しは「チェーン」されます。setInterval
では、各反復処理がチェーンの一部になります。次のコードを使うとわかりやすくなります。
let chainCount = 0;
setInterval(() => {
chainCount++;
console.log(`This is number ${chainCount} in the chain`);
}, 500);
および:
let chainCount = 0;
function setTimeoutChain() {
setTimeout(() => {
chainCount++;
console.log(`This is number ${chainCount} in the chain`);
setTimeoutChain();
}, 500);
}
スロットリングの仕組み
スロットリングは段階的に行われます。
最小スロットリング
これは、次のいずれかに該当する場合にスケジュール設定されたタイマーに適用されます。
- ページが表示可能である。
- 過去 30 秒間にページで音が鳴っています。これは、任意の音源作成 API からのものですが、無音音声トラックはカウントされません。
リクエストされたタイムアウトが 4 ミリ秒未満で、チェーン カウントが 5 以上(タイムアウトが 4 ミリ秒に設定されている場合)でない限り、タイマーはスロットリングされません。これは新しいものではありません。各ブラウザで長年にわたって行われてきました。
スロットリング
これは、最小スロットリングが適用されず、次のいずれかに該当する場合にスケジュール設定されたタイマーで発生します。
- チェーン数が 5 未満である。
- ページが 5 分以内に非表示になっている。
- WebRTC を使用しています。具体的には、「オープン」の
RTCDataChannel
または「ライブ」のMediaStreamTrack
のRTCPeerConnection
があります。
ブラウザはこのグループのタイマーを 秒に 1 回確認します。チェックは 1 秒に 1 回しか行われないため、同様のタイムアウトを持つタイマーはまとめてバッチ処理され、タブでコードを実行するのに必要な時間が集約されます。これも新しいものではありません。ブラウザはここ数年、ある程度の能力を持っています。
集中スロットリング
Chrome 88 の新機能です。集中スロットリングは、最小スロットリング条件またはスロットリング条件のいずれも当てはまらず、次の条件のすべてに該当したときにスケジュール設定されたタイマーで発生します。
- ページが 5 分以上非表示になっている。
- チェーン数が 5 以上。
- ページが 30 秒以上応答しない場合。
- WebRTC は使用されていません。
この場合、ブラウザはこのグループのタイマーを 1 分に 1 回確認します。 以前と同様に、これはタイマーがバッチで分単位のチェックを行うことを意味します。
回避策
通常は、タイマーに代わるより適切な代替手段があります。また、CPU とバッテリー駆動時間に配慮するために、タイマーを他のものと組み合わせて使用することもできます。
状態ポーリング
これがタイマーの最も一般的な(誤った)使用法であり、何かが変更されたかどうかを継続的にチェックまたはポーリングするために使用されます。ほとんどの場合、push と同等の機能があり、変更が発生したときにそのことが通知されるため、チェックを続ける必要はありません。同じ成果を達成できるイベントがあるかどうかを確認します。
たとえば次のような例が考えられます。
- 要素がいつビューポートに入るかを知る必要がある場合は、
IntersectionObserver
を使用します。 - 要素のサイズが変更されるタイミングを知る必要がある場合は、
ResizeObserver
を使用します。 - DOM の変化を知る必要がある場合は、
MutationObserver
またはカスタム要素のライフサイクル コールバックを使用します。 - サーバーをポーリングするのではなく、ウェブソケット、サーバー送信イベント、push メッセージ、またはストリームの取得を検討してください。
- 音声や動画のステージの変更に反応する必要がある場合は、
timeupdate
やended
などのイベントを使用するか、各フレームで何かを行う必要がある場合はrequestVideoFrameCallback
を使用します。
特定の時刻に通知を表示したい場合は、通知トリガーもあります。
アニメーション
アニメーションは視覚的な要素であるため、ページが非表示になっているときは CPU 時間を使用すべきではありません。
アニメーション処理のスケジュール設定には、JavaScript タイマーよりも requestAnimationFrame
の方がはるかに優れています。これはデバイスのリフレッシュ レートと同期するため、表示可能なフレームごとに 1 つのコールバックのみを取得し、そのフレームの作成に最大時間を確保できます。また、requestAnimationFrame
は、ページが表示されるのを待つため、ページが非表示のときは CPU を使用しません。
アニメーション全体を事前に宣言できる場合は、CSS アニメーションまたはウェブ アニメーション API の使用を検討してください。これらには requestAnimationFrame
と同じ利点がありますが、ブラウザは自動合成などの追加の最適化を実行でき、通常は簡単に使用できます。
アニメーションが低フレームレート(点滅するカーソルなど)の場合でも、現時点ではタイマーが最適なオプションですが、requestAnimationFrame
と組み合わせることで両方の長所を活かすことができます。
function animationInterval(ms, signal, callback) {
const start = document.timeline.currentTime;
function frame(time) {
if (signal.aborted) return;
callback(time);
scheduleFrame(time);
}
function scheduleFrame(time) {
const elapsed = time - start;
const roundedElapsed = Math.round(elapsed / ms) * ms;
const targetNext = start + roundedElapsed + ms;
const delay = targetNext - performance.now();
setTimeout(() => requestAnimationFrame(frame), delay);
}
scheduleFrame(start);
}
使用方法:
const controller = new AbortController();
// Create an animation callback every second:
animationInterval(1000, controller.signal, time => {
console.log('tick!', time);
});
// And stop it:
controller.abort();
テスト
この変更は、Chrome 88(2021 年 1 月)のすべての Chrome ユーザーに適用されます。 この機能は現在、Chrome Beta、Dev、Canary の 50% に対して有効になっています。テストするには、Chrome Beta、Dev、Canary の起動時に次のコマンドライン フラグを使用します。
--enable-features="IntensiveWakeUpThrottling:grace_period_seconds/10,OptOutZeroTimeoutTimersFromThrottling,AllowAggressiveThrottlingWithWebSocket"
grace_period_seconds/10
引数を指定すると、ページが非表示になるまでの 5 分前ではなく 10 秒後に強力なスロットリングが開始され、スロットリングの影響を確認しやすくなります。
今後の計画
タイマーは CPU の過剰な使用の原因となるため、ウェブ コンテンツを壊さずにタイマーをスロットリングする方法や、ユースケースに合わせて追加または変更できる API を引き続き検討します。個人的には、効率的な低頻度アニメーション コールバックを優先して、animationInterval
の必要性をなくしたいと考えています。ご不明な点がございましたら、Twitter でお問い合わせください。
ヘッダー写真: Heather Zabriskie(出典: Unsplash)