対応ブラウザ
最新のブラウザでは、システム リソースが制限されている場合、ページが一時停止されたり、完全に破棄されたりすることがあります。将来的には、ブラウザはこれをプロアクティブに行うことで、消費電力とメモリを削減したいと考えています。Page Lifecycle API はライフサイクル フックを提供するため、ページはユーザー エクスペリエンスに影響を与えることなく、これらのブラウザの介入を安全に処理できます。API を調べて、これらの機能をアプリに実装する必要があるかどうかを確認します。
背景
アプリケーションのライフサイクルは、最新のオペレーティング システムがリソースを管理する重要な手段になります。Android、iOS、最近の Windows バージョンでは、OS によってアプリをいつでも起動および終了できます。これにより、これらのプラットフォームでリソースを最適化して、ユーザーにとって最もメリットがある場所に再割り当てすることが可能になります。
ウェブでは、これまでこのようなライフサイクルがなく、アプリを無期限に実行したままにできました。実行されるウェブページの数が増加するにつれて、メモリ、CPU、バッテリー、ネットワークなどの重要なシステム リソースが過剰にサブスクライブされる場合があり、エンドユーザー エクスペリエンスの悪化につながっています。
ウェブ プラットフォームには、長い間、ライフサイクルの状態に関連するイベント(load
、unload
、visibilitychange
など)がありましたが、これらのイベントでは、ユーザーが開始したライフサイクルの状態の変化にのみデベロッパーが応答できます。低電力のデバイスでウェブが確実に機能し(すべてのプラットフォームでリソースをより意識した動作を実現するため)、ブラウザはシステム リソースを事前に再利用して再割り当てする方法が必要です。
実際、現在のブラウザでは、バックグラウンド タブのページに対してリソースを節約するための積極的な対策がすでに取られています。多くのブラウザ(特に Chrome)は、全体的なリソース使用量を削減するために、この対策をさらに強化したいと考えています。
問題は、デベロッパーがこのようなシステム主導型の介入に備える方法がないことです。また、介入が行われていることを把握することさえできません。つまり、ブラウザは保守的である必要があり、そうでないとウェブページが破損するリスクがあります。
Page Lifecycle API は、次のようにこの問題を解決しようとします。
- ウェブ上のライフサイクル状態のコンセプトを導入し、標準化する。
- システムによって開始される新しい状態を定義し、ブラウザが非表示または非アクティブなタブで消費できるリソースを制限できるようにします。
- ウェブ デベロッパーが、これらの新しいシステム開始状態との間の遷移に応答できるようにする新しい API とイベントを作成します。
このソリューションは、システムの介入に耐性のあるアプリケーションを構築するためにウェブ デベロッパーが必要とする予測可能性を提供し、ブラウザがシステム リソースをより積極的に最適化できるようにします。最終的には、すべてのウェブユーザーにメリットがもたらされます。
以降では、新しいページ ライフサイクル機能を紹介し、既存のすべてのウェブ プラットフォームの状態とイベントとの関係について説明します。また、各ステータスでデベロッパーが行うべき(および行わないべき)作業の種類に関する推奨事項とベスト プラクティスも示します。
ページのライフサイクルの状態とイベントの概要
ページのライフサイクルの状態はすべて個別で相互に排他的です。つまり、ページは一度に 1 つの状態にしかありません。ページのライフサイクル ステータスのほとんどの変更は、通常、DOM イベントで検出できます(例外については、各ステータスに関するデベロッパー向けの推奨事項をご覧ください)。
ページ ライフサイクルの状態と、それらの状態間の遷移を示すイベントを説明する最も簡単な方法は、図を使用することです。
州
各状態について、次の表で詳しく説明します。また、前後に発生する可能性のある状態と、開発者が変更を検出するために使用できるイベントも一覧表示されます。
州 | 説明 |
---|---|
有効 |
ページが可視で、入力フォーカスがある場合は、ページはアクティブ状態です。
考えられる以前の状態: |
受動 |
ページが可視状態で、入力フォーカスが設定されていない場合、そのページは非アクティブ状態です。
以前の状態:
次の状態の候補: |
非表示 |
ページが非表示(フリーズ、破棄、終了されていない)状態の場合、そのページは非表示状態です。
考えられる以前の状態:
次の状態の候補: |
フリーズ |
フリーズ状態の場合、ページがフリーズ解除されるまで、ブラウザはページの
タスクキュー内の
フリーズ可能
タスクの実行を停止します。つまり、JavaScript タイマーやフェッチ コールバックは実行されません。すでに実行中のタスクは終了できます(特に
ブラウザは、CPU / バッテリー / データ使用量を節約するためにページをフリーズします。また、 前後への移動を高速化するためにページ全体の再読み込みを回避するためにも、ページをフリーズします。
次の状態の候補: |
終了 |
ページがブラウザによってアンロードされ、メモリから消去され始めると、ページは終了状態になります。この状態では 新しいタスクを開始できず、実行時間が長すぎると進行中のタスクが強制終了される可能性があります。
次の状態の候補: |
破棄済み |
ページがリソースを節約するためにブラウザによってアンロードされると、そのページは破棄された状態になります。破棄は通常、新しいプロセスを開始できないリソース制約下で発生するため、この状態ではタスク、イベント コールバック、または任意の JavaScript を実行できません。 [破棄済み] 状態の場合、通常、ページは消去されていても、タブ自体(タブのタイトルやファビコンなど)はユーザーに表示されます。
以前の状態:
次の状態の候補: |
イベント
ブラウザは多くのイベントをディスパッチしますが、そのうちのごく一部のイベントのみが、ページのライフサイクルの状態が変化する可能性があることを示します。次の表に、ライフサイクルに関連するすべてのイベントの概要と、イベントが遷移する可能性のある状態を示します。
名前 | 詳細 |
---|---|
focus
|
DOM 要素がフォーカスを受け取った。
注:
考えられる以前の状態:
現在の状態の可能性: |
blur
|
DOM 要素のフォーカスが失われた。
注:
以前の状態:
現在の状態の可能性: |
visibilitychange
|
ドキュメントの
|
freeze
*
|
ページが凍結されました。ページのタスクキュー内の フリーズ可能なタスクは開始されません。
考えられる以前の状態:
考えられる現在の状態: |
resume
*
|
ブラウザがフリーズしたページを再開しました。
以前の状態:
現在の状態の例: |
pageshow
|
セッション履歴のエントリに移動中。 これは、まったく新しいページの読み込みか、バックフォワード キャッシュから取得されたページのいずれかです。ページがバック/フォワード キャッシュから取得された場合、イベントの |
pagehide
|
セッション履歴エントリの参照元。 ユーザーが別のページに移動し、ブラウザが現在のページをバック/フォワード キャッシュに追加して後で再利用できる場合は、イベントの
考えられる以前の状態:
現在の状態の可能性: |
beforeunload
|
ウィンドウ、ドキュメント、そのリソースがアンロードされようとしています。 この時点では、ドキュメントは引き続き表示され、イベントはキャンセル可能です。
重要:
考えられる以前の状態:
現在の状態の例: |
unload
|
ページがアンロードされている。
警告:
考えられる以前の状態:
現在の状態の可能性: |
* Page Lifecycle 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 以降、パソコン版 Chrome の document
オブジェクトに 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';
};
一方、凍結状態と終了状態は、状態が変化するときに、それぞれのイベント リスナー(freeze
と pagehide
)でのみ検出できます。
状態変化を監視する方法
前に定義した getState()
関数を基に、次のコードでページ ライフサイクルのすべての状態変化を監視できます。
// 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 以外の処理は、 アイドル状態に優先度を下げるか、 Web Worker にオフロードする必要があります。 |
Passive |
非アクティブ状態は、ユーザーがページを操作していない状態ですが、ページは表示されています。つまり、UI の更新とアニメーションは引き続きスムーズに行われる必要がありますが、これらの更新が発生するタイミングは重要ではありません。 ページがアクティブからパッシブに変わったら、保存されていないアプリケーションの状態を保持するのが適切なタイミングです。 |
ページが [非アクティブ] から [非表示] に変更されると、ページが再読み込みされるまでユーザーがそのページを操作しなくなる可能性があります。 非表示への遷移は、デベロッパーが確実に検出できる最後の状態変化である場合もあります(これは特にモバイルの場合に当てはまります。ユーザーがタブやブラウザ アプリ自体を閉じることができ、そのような場合、 つまり、[hidden] 状態は、ユーザーのセッションが終了する可能性が高い状態として扱う必要があります。つまり、保存されていないアプリケーションの状態を保持し、送信されていない分析データを送信します。 また、UI の更新も停止する必要があります(ユーザーには表示されないため)。また、ユーザーがバックグラウンドで実行したくないタスクも停止する必要があります。 |
|
Frozen |
凍結状態の場合、 タスクキュー内の フリーズ可能なタスクは、ページがフリーズ解除されるまで一時停止されます。これは、ページが破棄された場合など、発生しないこともあります。 つまり、ページが非表示から凍結に変更された場合は、タイマーを停止するか、接続を破棄する必要があります。凍結すると、同じオリジンで開いている他のタブに影響したり、ブラウザがページを 前後移動キャッシュに格納する機能に影響したりする可能性があります。 特に、次のことを行ってください。
また、動的ビューの状態(無限リストビューのスクロール位置など)を
ページが凍結から非表示に戻った場合は、閉じられた接続を再び開くことができます。また、ページが最初に凍結されたときに停止したポーリングを再開することもできます。 |
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
の違いの一つは、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.
return (event.returnValue = true);
}
});
代わりに、次のようにします(beforeunload
リスナーは必要な場合にのみ追加され、不要な場合は削除されるためです)。
const beforeUnloadListener = (event) => {
event.preventDefault();
// Legacy support for older browsers.
return (event.returnValue = true);
};
// A function that invokes a callback when the page has unsaved changes.
onPageHasUnsavedChanges(() => {
addEventListener('beforeunload', beforeUnloadListener);
});
// A function that invokes a callback when the page's unsaved changes are resolved.
onAllChangesSaved(() => {
removeEventListener('beforeunload', beforeUnloadListener);
});
よくある質問
「読み込み中」の状態が表示されないのはなぜですか?
Page Lifecycle API では、状態を個別かつ相互排他的に定義します。ページはアクティブ、パッシブ、非表示のいずれかの状態で読み込まれ、読み込みが完了する前に状態が変更されたり、終了したりする可能性があるため、このパラダイムでは個別の読み込み状態は意味がありません。
非表示のときに重要な処理を行うページが、フリーズまたは破棄されないようにするにはどうすればよいですか?
ウェブページが非表示の状態で実行されているときにフリーズしないようにする正当な理由はたくさんあります。最もわかりやすい例は、音楽を再生するアプリです。
また、Chrome がページを破棄することが危険な場合もあります。たとえば、送信されていないユーザー入力を含むフォームが含まれている場合や、ページのアンロード時に警告する beforeunload
ハンドラが含まれている場合などです。
現時点では、Chrome はページを破棄する際には慎重に行動しており、ユーザーに影響しないことが確実な場合にのみ破棄します。たとえば、非表示状態のときに次のいずれかを行うことが確認されたページは、リソースが極端に制限されている場合を除き、破棄されません。
- 音声を再生しています
- WebRTC の使用
- テーブルのタイトルまたはファビコンの更新
- アラートの表示
- プッシュ通知の送信
タブを安全にフリーズまたは破棄できるかどうかを判断するために使用される現在のリスト機能については、Chrome のフリーズと破棄のヒューリスティクスをご覧ください。
バックフォワード キャッシュは、一部のブラウザで実装されている、戻るボタンと進むボタンの使用を高速化するナビゲーションの最適化を表す用語です。
ユーザーがページから移動すると、これらのブラウザはそのページのバージョンをフリーズします。これにより、ユーザーが戻るボタンまたは進むボタンを使用して戻ってきた場合に、ページをすばやく再開できます。unload
イベント ハンドラを追加すると、この最適化が不可能になります。
あらゆる目的において、このフリーズは、CPU やバッテリーを節約するためにブラウザが行うフリーズと同じ機能です。そのため、このフリーズはライフサイクル状態の凍結の一部と見なされます。
非同期 API をフリーズ状態または終了状態のときに実行できない場合、IndexedDB にデータを保存するにはどうすればよいですか?
フリーズ状態と終了状態では、ページのタスクキュー内のフリーズ可能なタスクが停止されます。つまり、IndexedDB などの非同期およびコールバックベースの API を信頼性を持って使用することはできません。
今後、IDBTransaction
オブジェクトに commit()
メソッドを追加する予定です。これにより、デベロッパーは、コールバックを必要としない書き込み専用トランザクションを効果的に実行できるようになります。つまり、デベロッパーが IndexedDB にデータを書き込むだけで、読み取りと書き込みからなる複雑なトランザクションを実行していない場合、タスクキューが停止する前に commit()
メソッドを完了できます(IndexedDB データベースがすでに開いていることを前提としています)。
ただし、すぐに動作する必要があるコードについては、デベロッパーには次の 2 つの方法があります。
- セッション ストレージを使用する: セッション ストレージは同期であり、ページの破棄後も保持されます。
- Service Worker から IndexedDB を使用する: Service Worker は、ページが終了または破棄された後に IndexedDB にデータを保存できます。
freeze
またはpagehide
イベント リスナーで、postMessage()
を介して Service Worker にデータを送信できます。Service Worker は、データを保存できます。
アプリをフリーズ状態と破棄状態の両方でテストする
アプリがフリーズ状態と破棄状態のときにどのように動作するかをテストするには、chrome://discards
に移動して、開いているタブを実際にフリーズまたは破棄します。
これにより、ページが破棄後に再読み込みされたときに、freeze
イベントと resume
イベント、および document.wasDiscarded
フラグを正しく処理できます。
概要
ユーザーのデバイスのシステム リソースを尊重したいデベロッパーは、ページのライフサイクルの状態を考慮してアプリを作成する必要があります。ユーザーが予期しない状況で、ウェブページがシステム リソースを過剰に消費しないようにすることが重要です。
デベロッパーが新しい Page Lifecycle API の実装を開始すればするほど、ブラウザが使用されていないページをフリーズして破棄することが安全になります。つまり、ブラウザのメモリ、CPU、バッテリー、ネットワーク リソースの消費量が削減され、ユーザーにとってメリットがあります。