対応ブラウザ
- 68
- 79
- x
- x
最新のブラウザでは、システム リソースに制約があると、ページを一時停止したり、完全に破棄したりすることがあります。将来的には、ブラウザは電力やメモリの消費を抑えるために、これをプロアクティブに行うことが求められます。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()
を呼び出し、次の状態を渡します。
このコードで注目すべき点の 1 つは、すべてのイベント リスナーが window
に追加され、すべて {capture: true}
を渡すことです。これには次のような理由があります。
- すべてのページ ライフサイクル イベントのターゲットが同じというわけではありません。
pagehide
とpageshow
はwindow
で、visibilitychange
、freeze
、resume
はdocument
で、focus
とblur
は、それぞれの DOM 要素で呼び出されます。 - これらのイベントのほとんどはバブルにならないため、キャプチャしないイベント リスナーを共通の祖先要素に追加して、そのすべてを監視することはできません。
- キャプチャ フェーズはターゲット フェーズまたはバブルフェーズの前に実行されるため、そこにリスナーを追加すると、他のコードがキャンセルする前にリスナーが実行されます。
各州でのデベロッパー向けの推奨事項
デベロッパーにとって、ページのライフサイクルの状態を理解することと、コードでそれを観察する方法を知っておくことが重要です。行うべき作業の種類は、ページの状態によって大きく左右されるためです。
たとえば、ページが非表示状態にある場合、一時的な通知をユーザーに表示しても意味がないことは明らかです。この例は非常に明白ですが、それほど明白でない、列挙する価値のある推奨事項もあります。
状態 | デベロッパー向けの推奨事項 |
---|---|
Active |
アクティブ状態はユーザーにとって最も重要なタイミングであり、ページが ユーザー入力に反応する最も重要なタイミングです。 メインスレッドをブロックする可能性のある UI 以外の処理は、 アイドル期間にするか、 ウェブワーカーにオフロードする必要があります。 |
Passive |
パッシブ状態では、ユーザーはページを操作していませんが、閲覧は可能です。つまり、UI の更新とアニメーションはスムーズでなければなりませんが、更新が行われるタイミングはそれほど重要ではありません。 ページが「アクティブ」から「パッシブ」に変わったら、未保存のアプリケーション状態を保持することをおすすめします。 |
ページが「パッシブ」から「非表示」に変わった場合、再読み込みされるまでユーザーが再度操作しない可能性があります。 また、多くの場合、hidden への遷移は、デベロッパーが確実に確認可能な最後の状態変更になります(これは特にモバイルに当てはまります。モバイルでは、ユーザーがタブやブラウザアプリ自体を閉じることができるため、そのような場合には つまり、非表示状態を、ユーザーのセッションが終了する可能性が高いものとして扱います。つまり、未保存のアプリケーション状態を保持し、未送信の分析データを送信します。 また、UI の更新は(ユーザーには表示されないため)停止するとともに、ユーザーがバックグラウンドで実行したくないタスクはすべて停止する必要があります。 |
|
Frozen |
フリーズ状態の場合、 タスクキュー内の フリーズ可能なタスクは、ページのフリーズが解除されるまで停止されます。これは決して起こらない可能性があります(ページが破棄された場合など)。 つまり、ページが非表示からフリーズに変わった場合は、タイマーを停止するか、接続を破棄する必要があります。接続が凍結されると、同じオリジンで開いている他のタブに影響が及んだり、ブラウザの バックフォワード キャッシュへのページの保存に影響が出たりする可能性があります。 特に、次のことを行うことが重要です。
また、動的ビューの状態(無限リスト表示でのスクロール位置など)を、ページを破棄して再読み込みした場合に復元する
ページが「フリーズ」から「非表示」に戻った場合、閉じられた接続を再開するか、ページが最初にフリーズされたときに停止したポーリングを再開できます。 |
Terminated |
通常、ページが終了状態に移行しても、何もする必要はありません。 ユーザー操作の結果としてアンロードされるページは、終了状態になる前に常に非表示状態になるため、セッション終了ロジック(アプリケーション状態の永続化、アナリティクスへのレポートなど)を実行する必要があります。 また(非表示状態に関する推奨事項で説明したように)多くの場合、終了状態への移行を確実に検出できないため、終了イベント( |
Discarded |
ページが破棄される時点で、デベロッパーが「discarded」状態を監視することはできません。その理由は、ページは通常、リソースの制約によって破棄されるからです。ほとんどの場合、破棄イベントに応答してスクリプトを実行できるようにのみページの固定を解除するのは不可能だからです。 そのため、非表示からフリーズに変更する際の破棄の可能性に備え、ページ読み込み時に破棄されたページの復元に対応するには、 |
繰り返しになりますが、ライフサイクル イベントの信頼性と順序はすべてのブラウザで一貫して実装されているわけではないため、この表に記載されているアドバイスに従う最も簡単な方法は PageLifecycle.js を使用することです。
避けるべき以前のライフサイクル API
以下のイベントは、可能な限り避ける必要があります。
アンロード イベント
多くのデベロッパーは、unload
イベントを保証されたコールバックとして扱い、状態を保存し、分析データを送信するためのセッション終了シグナルとして使用します。ただし、特にモバイルでは、この方法は非常に信頼性に欠けます。unload
イベントは、モバイルのタブ スイッチャーからタブを閉じる、アプリ スイッチャーからブラウザアプリを閉じるなど、一般的なアンロードの多くの状況では発生しません。
このため、visibilitychange
イベントに基づいてセッションが終了するタイミングを判断し、隠れ状態をアプリとユーザーデータを保存する信頼できる最終時間と見なすことをおすすめします。
さらに、(onunload
または addEventListener()
を介して)登録済みの unload
イベント ハンドラが存在するだけで、ブラウザがページをバックフォワード キャッシュに格納できず、バックフォワード ロードを高速化できなくなる可能性があります。
最新のブラウザでは、常に pagehide
イベントを使用して、ページのアンロード(終了状態)を検出することをおすすめします。unload
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.
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 では、状態を個別に、相互に排他的と定義します。ページはアクティブ状態、パッシブ状態、隠し状態のいずれでも読み込めます。また、読み込みが完了する前に状態を変更するか、終了することもあるため、別の読み込み状態は、このパラダイムでは意味をなしません。
非表示になっているページは重要な処理を行っています。凍結または破棄されないようにするにはどうすればよいですか?
ウェブページが隠れ状態で実行されている間、ウェブページを凍結してはならない正当な理由はたくさんあります。最もわかりやすい例は、音楽を再生するアプリです。
また、ユーザー入力が未送信のフォームや、ページがアンロードされたときに警告する beforeunload
ハンドラがある場合など、Chrome がページを破棄することはリスクが高い場合があります。
当面の間、Chrome は慎重にページを破棄し、ユーザーに影響を与えないと確信できる場合にのみ破棄します。たとえば、隠れ状態にある間に次のいずれかの動作が確認されたページは、リソースに極端な制約がない限り、破棄されません。
- 音声の再生中
- WebRTC の使用
- 表のタイトルまたはファビコンの更新
- アラートを表示しています
- プッシュ通知の送信
タブを安全に凍結または破棄できるかどうか判断するために使用されている現在のリスト機能については、Chrome の凍結と破棄のヒューリスティックをご覧ください。
バックフォワード キャッシュとは、一部のブラウザで「戻る」ボタンと「進む」ボタンを高速化するために実装されているナビゲーション最適化を示す用語です。
これらのブラウザでは、ユーザーがページから移動すると、そのページのバージョンが凍結され、ユーザーが [戻る] または [進む] ボタンを使用して戻った場合に、すぐに再開できます。unload
イベント ハンドラを追加すると、この最適化が不可能になります。
このフリーズは、CPU やバッテリーを節約するためにブラウザが実行するのと同じ機能を持ちます。そのため、フリーズ ライフサイクル状態の一部と見なされます。
フリーズ状態または終了状態で非同期 API を実行できない場合、IndexedDB にデータを保存するにはどうすればよいですか?
フリーズ状態または終了状態では、ページのタスクキュー内の凍結可能なタスクは一時停止されます。つまり、IndexedDB などの非同期 API やコールバック ベースの 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、バッテリー、ネットワーク リソースの消費が少なくなり、ユーザーにとってメリットがあります。