Browser Support
最近のブラウザでは、システム リソースが制約されている場合、ページが一時停止されたり、完全に破棄されたりすることがあります。今後、ブラウザはこれをプロアクティブに行い、消費電力とメモリを削減したいと考えています。Page Lifecycle API は、ページがユーザー エクスペリエンスに影響を与えることなく、ブラウザの介入を安全に処理できるように、ライフサイクル フックを提供します。API を確認して、これらの機能をアプリケーションに実装する必要があるかどうかを確認してください。
背景
アプリケーションのライフサイクルは、最新のオペレーティング システムがリソースを管理する重要な方法です。Android、iOS、最新の Windows バージョンでは、アプリは OS によっていつでも開始および停止できます。これにより、これらのプラットフォームはリソースを効率化し、ユーザーに最適な場所に再配分できます。
ウェブにはこれまでそのようなライフサイクルがなく、アプリは無期限に存続させることができます。多数のウェブページが実行されていると、メモリ、CPU、バッテリー、ネットワークなどの重要なシステム リソースが過剰に消費され、エンドユーザーの利便性が損なわれる可能性があります。
ウェブ プラットフォームには、load、unload、visibilitychange など、ライフサイクル状態に関連するイベントが以前から存在していましたが、これらのイベントでは、デベロッパーはユーザーが開始したライフサイクル状態の変更にしか対応できませんでした。ウェブが低電力デバイスで確実に動作し(すべてのプラットフォームでリソースをより意識するためにも)、ブラウザにはシステム リソースを事前に再利用して再割り当てする方法が必要です。
実際、現在のブラウザはすでにバックグラウンド タブのページに対してリソースを節約するための積極的な対策を講じており、多くのブラウザ(特に Chrome)は、全体的なリソース フットプリントを削減するために、この対策をさらに強化したいと考えています。
問題は、デベロッパーがこのようなシステム主導の介入に備える方法がないこと、また、介入が発生していることさえ把握できないことです。つまり、ブラウザは慎重に対応する必要があり、そうしないとウェブページが破損する可能性があります。
Page Lifecycle API は、次の方法でこの問題の解決を試みます。
- ウェブでのライフサイクル状態のコンセプトの導入と標準化。
- ブラウザが非表示または非アクティブなタブで使用できるリソースを制限できるようにする、システムが開始する新しい状態を定義します。
- ウェブ デベロッパーがこれらの新しいシステム開始状態への移行と移行元に対応できるようにする新しい API とイベントを作成します。
このソリューションは、システム介入に強いアプリケーションを構築するためにウェブ デベロッパーが必要とする予測可能性を提供し、ブラウザがシステム リソースをより積極的に最適化できるようにすることで、最終的にすべてのウェブ ユーザーにメリットをもたらします。
この記事の残りの部分では、新しいページ ライフサイクル機能を紹介し、既存のすべてのウェブ プラットフォームの状態とイベントとの関係について説明します。また、各状態においてデベロッパーがすべきこと(すべきでないこと)の種類に関する推奨事項とベスト プラクティスも提供します。
ページのライフサイクルの状態とイベントの概要
Page Lifecycle の状態はすべて個別の相互排他的な状態です。つまり、ページは一度に 1 つの状態にしかなれません。また、ページのライフサイクル状態のほとんどの変更は、一般的に DOM イベントを介して観測できます(例外については、各状態に関するデベロッパー向けのおすすめをご覧ください)。
ページ ライフサイクルの状態と、それらの状態間の遷移を示すイベントを説明する最も簡単な方法は、おそらく次の図を使用することです。
州
次の表で、各状態について詳しく説明します。また、前後の状態と、変更を監視するためにデベロッパーが使用できるイベントも一覧表示されます。
| 州 | 説明 |
|---|---|
| 有効 |
ページが可視で、入力フォーカスがある場合、そのページはアクティブ状態です。
考えられる以前の状態: |
| Passive |
ページが可視で入力フォーカスがない場合、ページはパッシブ状態になります。
考えられる前の状態:
次の状態: |
| 非表示 |
ページが非表示の場合(フリーズ、破棄、終了されていない場合)、ページは hidden 状態になります。
考えられる前の状態:
次の状態: |
| フリーズ |
フリーズ状態では、ページがフリーズ解除されるまで、ブラウザはページの
タスクキュー内の
フリーズ可能なタスクの実行を一時停止します。つまり、JavaScript タイマーや fetch コールバックなどは実行されません。すでに実行中のタスクは完了できます(特に
ブラウザは、CPU、バッテリー、データ使用量を節約するためにページをフリーズします。また、 前後のナビゲーションを高速化するためにもフリーズします。これにより、ページ全体を再読み込みする必要がなくなります。
考えられる前の状態:
次の状態: |
| 終了 |
ブラウザによってアンロードが開始され、メモリからクリアされると、ページは終了状態になります。この状態では、 新しいタスクを開始できません。また、進行中のタスクが長時間実行されている場合は、強制終了されることがあります。
考えられる前の状態:
考えられる次の状態: |
| 破棄済み |
リソースを節約するためにブラウザによってアンロードされたページは、破棄状態になります。この状態では、タスク、イベント コールバック、JavaScript などは実行できません。破棄は通常、リソースの制約下で発生し、新しいプロセスを開始することはできません。 破棄状態では、ページはなくなっても、通常はタブ自体(タブのタイトルやファビコンを含む)はユーザーに表示されます。
考えられる前の状態:
考えられる次の状態: |
イベント
ブラウザは多くのイベントをディスパッチしますが、そのうちのほんの一部だけがページのライフサイクル状態の変化の可能性を示します。次の表に、ライフサイクルに関連するすべてのイベントと、それらのイベントで遷移する可能性のある状態を示します。
| 名前 | 詳細 |
|---|---|
focus
|
DOM 要素がフォーカスを受け取りました。
注:
考えられる前の状態:
考えられる現在の状態: |
blur
|
DOM 要素のフォーカスが失われました。
注:
考えられる前の状態:
考えられる現在の状態: |
visibilitychange
|
ドキュメントの
|
freeze
*
|
ページが凍結されたばかりです。ページのタスクキュー内の フリーズ可能なタスクは開始されません。
考えられる前の状態:
考えられる現在の状態: |
resume
*
|
ブラウザがフリーズしたページを再開しました。
考えられる前の状態:
現在の状態の可能性: |
pageshow
|
セッション履歴のエントリがトラバースされています。 これは、新しいページの読み込みか、バックフォワード キャッシュから取得されたページのいずれかです。ページがバックフォワード キャッシュから取得された場合、イベントの |
pagehide
|
セッション履歴のエントリがトラバースされています。 ユーザーが別のページに移動し、ブラウザが現在のページを後で再利用できるようにバックフォワード キャッシュに追加できる場合、イベントの
考えられる前の状態:
現在の状態の候補: |
beforeunload
|
ウィンドウ、ドキュメント、およびそのリソースがアンロードされようとしています。この時点では、ドキュメントは引き続き表示され、イベントは引き続きキャンセル可能です。
重要:
考えられる前の状態:
考えられる現在の状態: |
unload
|
ページがアンロードされています。
警告:
考えられる前の状態:
考えられる現在の状態: |
* ページ ライフサイクル API で定義された新しいイベントを示します
Chrome 68 で追加された新機能
上のグラフには、ユーザーが開始したのではなくシステムが開始した 2 つの状態(フリーズと破棄)が示されています。前述のように、現在のブラウザはすでに(独自の判断で)非表示のタブをフリーズして破棄することがありますが、デベロッパーはそれがいつ発生したかを知ることはできません。
Chrome 68 では、デベロッパーは document の freeze イベントと resume イベントをリッスンすることで、非表示のタブがフリーズしたときとフリーズが解除されたときを観測できるようになりました。
document.addEventListener('freeze', (event) => {
// The page is now frozen.
});
document.addEventListener('resume', (event) => {
// The page has been unfrozen.
});
Chrome 68 以降、document オブジェクトにデスクトップ版 Chrome の wasDiscarded プロパティが含まれるようになりました(Android のサポートについては、こちらの問題で追跡しています)。非表示タブでページが破棄されたかどうかを判断するには、ページ読み込み時にこのプロパティの値を調べます(注: 破棄されたページを再度使用するには、再読み込みする必要があります)。
if (document.wasDiscarded) {
// Page was previously discarded by the browser while in a hidden tab.
}
freeze イベントと resume イベントで重要なことや、ページが破棄される場合の処理と準備については、各状態に関するデベロッパー向けのおすすめをご覧ください。
次のいくつかのセクションでは、これらの新機能が既存のウェブ プラットフォームの状態とイベントにどのように適合するかについて概要を説明します。
コードでページ ライフサイクルの状態を監視する方法
アクティブ、パッシブ、非表示の状態では、既存のウェブ プラットフォーム API から現在のページ ライフサイクル状態を判断する JavaScript コードを実行できます。
const getState = () => {
if (document.visibilityState === 'hidden') {
return 'hidden';
}
if (document.hasFocus()) {
return 'active';
}
return 'passive';
};
一方、frozen 状態と terminated 状態は、状態が変化する際に、それぞれのイベント リスナー(freeze と pagehide)でのみ検出できます。
状態の変化を監視する方法
前に定義した getState() 関数を基に、次のコードを使用してすべての PageLifecycle 状態の変更を観察できます。
// Stores the initial state using the `getState()` function (defined above).
let state = getState();
// Accepts a next state and, if there's been a state change, logs the
// change to the console. It also updates the `state` value defined above.
const logStateChange = (nextState) => {
const prevState = state;
if (nextState !== prevState) {
console.log(`State change: ${prevState} >>> ${nextState}`);
state = nextState;
}
};
// Options used for all event listeners.
const opts = {capture: true};
// These lifecycle events can all use the same listener to observe state
// changes (they call the `getState()` function to determine the next state).
['pageshow', 'focus', 'blur', 'visibilitychange', 'resume'].forEach((type) => {
window.addEventListener(type, () => logStateChange(getState()), opts);
});
// The next two listeners, on the other hand, can determine the next
// state from the event itself.
window.addEventListener('freeze', () => {
// In the freeze event, the next state is always frozen.
logStateChange('frozen');
}, opts);
window.addEventListener('pagehide', (event) => {
// If the event's persisted property is `true` the page is about
// to enter the back/forward cache, which is also in the frozen state.
// If the event's persisted property is not `true` the page is
// about to be unloaded.
logStateChange(event.persisted ? 'frozen' : 'terminated');
}, opts);
このコードは次の 3 つの処理を行います。
getState()関数を使用して初期状態を設定します。- 次の状態を受け取り、変更がある場合は状態の変更をコンソールに記録する関数を定義します。
- 必要なすべてのライフサイクル イベントにキャプチャ イベント リスナーを追加します。これにより、
logStateChange()が呼び出され、次の状態が渡されます。
コードについて注意すべき点は、すべてのイベント リスナーが window に追加され、すべてが {capture: true} を渡すことです。これには次のような理由があります。
- すべてのページ ライフサイクル イベントのターゲットが同じとは限りません。
pagehideとpageshowはwindowで発生し、visibilitychange、freeze、resumeはdocumentで発生し、focusとblurはそれぞれの DOM 要素で発生します。 - これらのイベントのほとんどはバブリングしないため、共通の祖先要素にキャプチャしないイベント リスナーを追加して、それらをすべて監視することはできません。
- キャプチャ フェーズはターゲット フェーズまたはバブル フェーズの前に実行されるため、リスナーをキャプチャ フェーズに追加すると、他のコードでキャンセルされる前に実行されるようになります。
各状態に対するデベロッパーの推奨事項
デベロッパーは、ページ ライフサイクルの状態を理解し、コードでそれらを監視する方法を知っておくことが重要です。なぜなら、実行すべき(および実行すべきでない)作業の種類は、ページの現在の状態に大きく依存するからです。
たとえば、ページが非表示状態の場合にユーザーに一時的な通知を表示するのは明らかに意味がありません。この例はかなり明白ですが、それほど明白ではない推奨事項も列挙する価値があります。
| 州 | デベロッパーの推奨事項 |
|---|---|
Active |
アクティブ状態はユーザーにとって最も重要な時間であり、ページが ユーザー入力に反応するうえで最も重要な時間です。 メインスレッドをブロックする可能性のある UI 以外の処理は、 アイドル期間に優先度を下げるか、 ウェブ ワーカーにオフロードする必要があります。 |
Passive |
パッシブ状態では、ユーザーはページを操作していませんが、ページは表示されています。つまり、UI の更新とアニメーションは引き続きスムーズに動作しますが、これらの更新が発生するタイミングはそれほど重要ではありません。 ページがアクティブからパッシブに変わるときは、保存されていないアプリの状態を永続化するのに適したタイミングです。 |
|
ページが passive から hidden に変わると、リロードされるまでユーザーがそのページを操作しなくなる可能性があります。 非表示への移行は、デベロッパーが確実に観察できる最後の状態変化であることが多いです(特にモバイルでは、ユーザーがタブやブラウザアプリ自体を閉じることができ、その場合は つまり、非表示状態は、ユーザーのセッションが終了する可能性が高い状態として扱う必要があります。つまり、保存されていないアプリケーションの状態を保持し、送信されていない分析データを送信します。 また、UI の更新も停止する必要があります(ユーザーには表示されないため)。さらに、ユーザーがバックグラウンドで実行してほしくないタスクも停止する必要があります。 |
|
Frozen |
フリーズ状態では、 タスクキュー内の フリーズ可能なタスクは、ページがフリーズ解除されるまで一時停止されます。フリーズ解除は行われない可能性があります(ページが破棄された場合など)。 つまり、ページが 非表示からフリーズに変わるときは、フリーズ状態になると同じオリジンの他の開いているタブに影響したり、ブラウザがページを 前後のキャッシュに保存する能力に影響したりする可能性のあるタイマーを停止したり、接続を解除したりすることが不可欠です。 特に、以下の点に注意してください。
また、ページが破棄されて後で再読み込みされた場合に復元したい動的ビューの状態(無限リストビューのスクロール位置など)は、
ページがフリーズ状態から非表示状態に戻った場合は、閉じられた接続を再開したり、ページが最初にフリーズしたときに停止したポーリングを再開したりできます。 |
Terminated |
通常、ページが terminated 状態に移行したときに、アクションを実行する必要はありません。 ユーザー アクションの結果としてアンロードされるページは、終了状態になる前に必ず非表示状態になるため、セッション終了ロジック(アプリケーション状態の永続化やアナリティクスへのレポートなど)は非表示状態で行う必要があります。 また(非表示状態に関する推奨事項で説明したように)、多くの場合(特にモバイルの場合)、終了状態への移行を確実に検出することはできないため、終了イベント( |
Discarded |
ページが破棄されるときに、デベロッパーが破棄状態を観察することはできません。これは、通常、ページはリソースの制約下で破棄されるためです。破棄イベントに応じてスクリプトを実行できるようにするためにページをフリーズ解除することは、ほとんどの場合不可能です。 そのため、非表示からフリーズへの変更で破棄される可能性に備える必要があります。また、 |
繰り返しになりますが、ライフサイクル イベントの信頼性と順序はすべてのブラウザで一貫して実装されているわけではないため、表の推奨事項に従う最も簡単な方法は PageLifecycle.js を使用することです。
避けるべき以前のライフサイクル API
次のイベントは、可能な限り避ける必要があります。
アンロード イベント
多くのデベロッパーは unload イベントを保証されたコールバックとして扱い、セッション終了のシグナルとして使用して状態を保存し、分析データを送信しますが、これは特にモバイルでは非常に信頼性が低い方法です。unload イベントは、モバイルのタブ切り替えからタブを閉じる場合や、アプリ切り替えからブラウザアプリを閉じる場合など、一般的なアンロードの状況では発生しません。
そのため、セッションの終了を判断する際は常に visibilitychange イベントに依存し、非表示状態をアプリとユーザーデータを保存する最後の信頼できる時間と考えることをおすすめします。
さらに、登録された unload イベント ハンドラ(onunload または addEventListener() のいずれかによる)が存在するだけで、ブラウザがページをバックフォワード キャッシュに保存して、前後のページの読み込みを高速化できなくなる可能性があります。
最新のブラウザでは、unload イベントではなく、pagehide イベントを使用して、ページがアンロードされる可能性(終了状態)を検出することを常におすすめします。Internet Explorer バージョン 10 以前をサポートする必要がある場合は、pagehide イベントの機能検出を行い、ブラウザが pagehide をサポートしていない場合にのみ unload を使用する必要があります。
const terminationEvent = 'onpagehide' in self ? 'pagehide' : 'unload';
window.addEventListener(terminationEvent, (event) => {
// Note: if the browser is able to cache the page, `event.persisted`
// is `true`, and the state is frozen rather than terminated.
});
beforeunload イベント
beforeunload イベントには unload イベントと同様の問題があります。過去に beforeunload イベントが存在すると、ページがバックフォワード キャッシュの対象にならなくなる可能性がありました。最新のブラウザにはこの制限はありません。ただし、一部のブラウザでは、ページをバック/フォワード キャッシュに保存しようとすると、予防措置として beforeunload イベントが発火されません。つまり、このイベントはセッション終了のシグナルとしては信頼性が低いということです。また、一部のブラウザ(Chrome など)では、beforeunload イベントを発生させる前にページでのユーザー操作が必要になるため、信頼性がさらに低下します。
beforeunload と unload の違いの 1 つは、beforeunload には正当な用途があることです。たとえば、ページをアンロードすると保存されていない変更が失われることをユーザーに警告する場合などです。
beforeunload を使用する正当な理由があるため、ユーザーが保存していない変更がある場合にのみ beforeunload リスナーを追加し、保存後にすぐに削除することをおすすめします。
つまり、次のような処理は行わないでください(beforeunload リスナーを無条件に追加するため)。
addEventListener('beforeunload', (event) => {
// A function that returns `true` if the page has unsaved changes.
if (pageHasUnsavedChanges()) {
event.preventDefault();
// Legacy support for older browsers.
event.returnValue = true;
}
});
代わりに、次のようにします(必要なときにのみ beforeunload リスナーを追加し、不要になったら削除するため)。
const beforeUnloadListener = (event) => {
event.preventDefault();
// Legacy support for older browsers.
event.returnValue = true;
};
// A function that adds a `beforeunload` listener if there are unsaved changes.
onPageHasUnsavedChanges(() => {
addEventListener('beforeunload', beforeUnloadListener);
});
// A function that removes the `beforeunload` listener when the page's unsaved
// changes are resolved.
onAllChangesSaved(() => {
removeEventListener('beforeunload', beforeUnloadListener);
});
よくある質問
「読み込み中」の状態がないのはなぜですか?
Page Lifecycle API は、状態を個別の相互排他的なものとして定義します。ページはアクティブ、パッシブ、非表示のいずれかの状態で読み込まれる可能性があり、読み込みが完了する前に状態が変化したり、終了したりする可能性もあるため、このパラダイムでは読み込み状態を別途設けることは意味がありません。
ページが非表示のときに重要な処理を行っています。フリーズや破棄を防ぐにはどうすればよいですか?
ウェブページが非表示状態で実行されているときにフリーズしないようにする正当な理由はたくさんあります。最もわかりやすい例は、音楽を再生するアプリです。
Chrome がページを破棄すると危険な場合もあります。たとえば、送信されていないユーザー入力を含むフォームが含まれている場合や、ページのアンロード時に警告する beforeunload ハンドラがある場合などです。
当面の間、Chrome はページを破棄する際に慎重になり、ユーザーに影響を与えないと確信できる場合にのみ破棄します。たとえば、非表示状態のときに次のいずれかの動作が確認されたページは、リソースが極端に制約されている場合を除き、破棄されません。
- 音声の再生
- WebRTC の使用
- テーブルのタイトルまたはファビコンの更新
- アラートの表示
- プッシュ通知の送信
タブを安全にフリーズまたは破棄できるかどうかを判断するために使用される現在のリスト機能については、Chrome のフリーズと破棄のヒューリスティックをご覧ください。
バックフォワード キャッシュとは、一部のブラウザで実装されているナビゲーションの最適化を指す用語で、戻るボタンと進むボタンの使用を高速化します。
ユーザーがページから移動すると、これらのブラウザはそのページのバージョンをフリーズし、ユーザーが戻るボタンまたは進むボタンを使用して戻った場合にすばやく再開できるようにします。unload イベント ハンドラを追加すると、この最適化はできなくなることに注意してください。
このフリーズは、CPU/バッテリーを節約するためにブラウザが行うフリーズと機能的に同じです。そのため、フリーズ ライフサイクル状態の一部と見なされます。
フリーズ状態または終了状態では非同期 API を実行できない場合、IndexedDB にデータを保存するにはどうすればよいですか?
フリーズ状態と終了状態では、ページのタスクキュー内のフリーズ可能なタスクが一時停止されます。つまり、非同期 API とコールバック ベースの API を確実に使用できません。
IndexedDB API のほとんどはコールバック ベースですが、IDBTransaction インターフェースの commit() メソッドは、未処理のリクエストからのイベントがディスパッチされるのを待たずに、アクティブなトランザクションでコミット プロセスを開始する方法を提供します。これにより、コミットが別のタスクでキューに登録されるのではなく、すぐに実行されるため、freeze または visibilitychange イベント リスナーで IndexedDB データベースにデータを保存する信頼性の高い方法が提供されます。
フリーズ状態と破棄状態でのアプリのテスト
アプリがフリーズ状態と破棄状態になったときの動作をテストするには、chrome://discards にアクセスして、開いているタブを実際にフリーズまたは破棄します。
これにより、破棄後にページが再読み込みされたときに、ページが freeze イベントと resume イベント、および document.wasDiscarded フラグを正しく処理していることを確認できます。
概要
ユーザーのデバイスのシステム リソースを尊重したいデベロッパーは、ページ ライフサイクルの状態を考慮してアプリを構築する必要があります。ユーザーが想定していない状況でウェブページが過剰なシステム リソースを消費しないようにすることが重要です。
新しい Page Lifecycle API を実装するデベロッパーが増えるほど、ブラウザが使用されていないページをフリーズして破棄する際の安全性が高まります。つまり、ブラウザが消費するメモリ、CPU、バッテリー、ネットワーク リソースが減るため、ユーザーにとってメリットがあります。