JavaScript のライフサイクルに関するイベント
はじめに
ここでは、ウェブページのライフサクル(ページの読み込みから開放まで)に関する、 JavaScript のイベント(実行タイミング)の覚書です。
onLazy.js の開発で得られた知見をまとめて記載します。
基本(script)
<script>
を発見した場合の処理- type 属性が
text/javascript
以外の場合、スクリプトは実行されない- type 属性未定義は、
text/javascript
として解釈される
- type 属性未定義は、
- async 属性がある場合、スクリプトの読み込みと実行を非同期で実行する
- src 属性が必須
- HTML の解析処理は、スクリプトの処理と平行して継続する
- スクリプトは、スクリプトの読み込み後に実行する
- HTML の解析処理中に実行されることがある
- HTML の解析処理完了後に実行されることもある
- async 属性のある
<script>
の実行順は、保証されない
- defer 属性がある場合、スクリプトの読み込みを非同期で実行する
- src 属性が必須
- HTML の解析処理は、スクリプトの読み込みと平行して継続する
- HTML の解析完了後にスクリプトを実行する
- defer 属性のある
<script>
の実行順は、保証される
- defer 属性のある
- async 属性と併記した場合、ブラウザが対応していれば async 属性が優先される
- src 属性がある場合、ファイルの読み込みを待機して、スクリプトを実行する
- スクリプト実行後に HTML の解析処理を再開する
- async / defer 属性がある場合、 async / defer 属性として処理される
- 上記以外の場合、 HTML の解析処理を中断する(タスクが分割される)
- スクリプトを即座に実行する
- type 属性が
※ async / defer について、次の記事が参考になります
<script> タグに async / defer を付けた場合のタイミング - Qiita
※ module/module async
属性がある
実行順だけに着目すれば、 defer/async
属性と同じ
外部スタイルシートによるスクリプトのブロック
外部ファイルのスタイルシート宣言が先にある場合、スタイルシートの読み込み完了まで <script>
の実行を待機します。これは、スクリプト内でスタイルシートを参照する可能性があるための措置です。
スクリプトがスタイルシートに依存しない場合、 <link type="stylesheet">
より前に <script>
を記載することでこの問題を回避できます。
動的ロード
<head>
<script>
var script = document.createElement('script');
script.src = 'script.js';
document.head.appendChild(script);
</script>
<body>
※同期実行はしない。 async 属性と類似する動作となる。
埋め込みスクリプトの実行箇所を判別する
if (!document.body) {
// <head>内で実行
}
if (document.readyState === 'loading') {
// <body>内で実行
}
※ <script>
を HTML に直接記述した場合の判定方法です。
基本(状態遷移)
読み込み~開放までの状態遷移です。 focus / blur, visibilitychange, beforeunload / pagehide / unload, freeze / resume / pageshow, など複雑なイベントの状態遷移があります。主にフォーカス、表示非表示、アンロード、破棄と開放、再表示に関する状態を表しています。
次の記事が参考になります。
readystatechange イベント
document.readyState
の値変更時に発火する。
document.readyState
は、 loading/interactive/complete
の3つの値に遷移します。 readystatechange イベントは、 interactive/complete
変更時の合計2回呼び出されます。 interactive
変更時は、 HTML の解析完了直後で defer 属性のスクリプトの実行直前です。その後、 DOMContentLoaded イベントが実行されます。 complete
変更時は、loadイベントの実行直前です。
※ document.readyState
の値は、 readystatechange イベント直前に変更されます。
DOMContentLoaded イベント
最初の HTML 文書の読み込みと解析が完了時に発火する。
HTML の </html>
まで解析が完了後に呼び出されます。 DOMContentLoaded イベント後に、リソース(外部ファイル等)の読み込みを実施します。
HTML の解析が完了しているため、 DOM が既に完成しているため、ドキュメントに関する処理を実施できます。
<script>
の直接記述
src 属性なしの <script>
を直接記述した場合、スクリプトはその時点で実行されます。
その時点で実行されるため、まだ DOM が完全には完成していません。そのため、 <script>
より後に記述のある DOM 要素は存在しません。
これを回避するため、 </body>
直前に <script>
を記述したり、 DOMContentLoaded
イベントに登録だけして実行を後回しにする回避策が存在します。
※DOMContentLoaded
イベント後に DOMContentLoaded
の登録を実施してもイベントは実行されません。(既に発生済みのため、)イベントのタイミングが発生しません。
DOMContentLoaded イベントの完了を判定する
if (document.readyState === 'loading') {
// DOMContentLoaded イベント前
} else {
// DOMContentLoaded イベント後
// 例外:readystatechange(interactive)/defer属性/DOMContentLoadedの実行中を含む
}
load イベント
リソースの読み込み完了時に発火する。
load
イベントは、ページ構成や通信状態によっては問題となるほど遅くに発火します。例として、ページ読み込み込みから1秒後に初回描画して、10秒後にload
イベントが発生したとしても何も不思議はありません。そのため、ページ読み込み後の処理は、できうる限り DOMContentLoaded
イベントで実施すべきです。
load イベントの完了を判定する
if (document.readyState !== 'complete') {
// load イベント前
} else {
// load イベント後
// 例外:readystatechange(complete)/loadイベント中を含む
}
pageshow イベント
pageshow イベントは、 load イベントと類似しています。
初回の pageshow イベントは、 load イベントの発動直後に発火します。初回の pageshow イベントは、 persisted
に true
が設定されています。
初回以降にページがロードされた場合、 pageshow イベントは persisted
に false
が設定されています。初回以外のページロードは、戻る機能などでページがキャッシュされていた場合に発生します。(ページがキャッシュされていた場合、 load イベントは発生しません)
pageshow イベントは、 load イベント後に実行されます。 DOMContentLoaded → load → pageshow の順にイベントが発生します。
First CPU Idle
<head>
<script>
window.requestIdleCallback(function() {
// First CPU Idle
});
</script>
※requestIdleCallback - Web API | MDN
※window.requestIdleCallback
は、アイドル処理の直前に呼び出される。
<head>
内の埋め込みスクリプトは、初回アイドル前である可能性が非常に高い。
そのため、初回アイドルを補足できる(初回アイドルでない可能性もある)
First Contentful Paint (FCP)
FCP直前 / FCP直後
<head>
<script>
window.requestAnimationFrame(function() {
// FCP直前
window.requestAnimationFrame(function() {
// FCP直後(ただし、描画のフレームがスキップされる可能性がある)
});
});
</script>
※Window.requestAnimationFrame() - Web API | MDN
※window.requestAnimationFrame
は、描画処理の直前に呼び出される。
<head>
内の埋め込みスクリプトは、FCP前が保証できる。
よって、次の描画処理は FCP の直前となる。
ただし、描画のフレームがスキップされる可能性がある。
FCP後
new PerformanceObserver(function(entryList) {
// FCP後(ただし、FCP直後とは限らない。FCP後すぐ実行される保証はない)
}).observe({type:'paint', buffered:true});
※PerformanceObserver() - Web API | MDN
※Largest Contentful Paint (LCP) も同様に検出できる
First Input Delay (FID)
FID後
new PerformanceObserver((entryList) => {
// FID後(ただし、FID直後とは限らない。FID後すぐ実行される保証はない)
}).observe({type: 'first-input', buffered: true});
初回ユーザイベント
click / mousedown / keydown / touchstart / mousemove / scroll
などのイベントをページ読み込み後に初めて取得したタイミング
onLazy.jsの主たる機能の1つ。
※FID で使用されるユーザイベントは、 click / mousedown / keydown / touchstart / pointerdown
です。
mousemove / scroll
がないことに留意する。
初回スクロール
scroll イベントをページ読み込み後に初めて取得したタイミング
onLazy.jsの主たる機能の1つ。
※ scroll イベントは、 DOMContentLoaded / load イベント前に発生することもある。
対象要素が表示領域内に入ったタイミング
let options = {
root: document.querySelector('#scrollArea'),
rootMargin: '0px',
threshold: 1.0
}
let observer = new IntersectionObserver(callback, options);
beforeunload イベント
ページがアンロードされる直前に発火する。そして、ダイアログを表示してページ遷移をキャンセルするか確認できる。
※ページとユーザーの対話が存在しない場合、例え beforeunload イベントを設定していてもダイアログは表示されません。これは、ユーザーとの対話が存在しない場合、ページ上から失われるデータが存在しないため、ページ遷移をキャンセルする理由そのものがなくなるためです。
beforeunload を設定したページをクリックあり・なしでクローズすることでこの動作を確認できます。
beforeunload のダイアログが出現しないことがある
pagehide イベント
pagehide イベントは、 unload イベントと類似しています。
初回の pagehide イベントは、 unload イベントの発動直前に発火します。初回の pagehide イベントは、 persisted
に true
が設定されています。
初回以降にページがアンロードされた場合、 pagehide イベントは persisted
に false
が設定されています。初回以外のページのアンロードは、戻る機能などでページがキャッシュされていた場合に発生します。
※unload イベントを設定した場合、ページはキャッシュされなくなります。
unload イベント
文書または子リソースがアンロードされるときに発生します。
unload
イベントの処理は、実行が完了することが保証されません。そのため、時間のかかる処理は、 pagehide
イベントで実行するべきです。
visibilitychange イベント
タブの表示非表示に関するイベントです。
ウィンドウの別タブに切り替える、または対象タブに戻ってきた場合に発火します。
※document.visibilityState
は、ページの可視性を示します。
※document.hidden
は、ページが非表示になっているかを示します。
focus / focusin / focusout / blur イベント
フォーカスを取得 / 失った時に発火します。
※document.activeElement
は、現在フォーカスしている要素を返します。
※document.hasFocus()
は、ドキュメントのフォーカスを判定できます。
※CORS制限付き外部iframeのfocusイベントを取得する
備考
addEventListener の一番最初に処理を実行する
window.addEventListener('DOMContentLoaded', function() {
// DOMContentLoadedイベントの最初に処理する
}, true);
キャプチャフェーズに登録することでより早くイベントを実行します。window
に登録することで他の要素より先にイベントを実行します。
※同様の手順で登録した場合、前に登録したものがより前に実行される。
addEventListener
は、登録順の実行が保証される。
※DOMContentLoaded
以外でも同様の方法が使用できる。
addEventListener の一番最後に処理を実行する
window.addEventListener('DOMContentLoaded', function() {
window.addEventListener('DOMContentLoaded', function() {
// DOMContentLoadedイベントの最後に処理する
}, false);
}, true);
※同様の手順で登録した場合、後に登録したものがより後に実行される。
addEventListener
は、登録順の実行が保証される。
キャプチャ・バブリングフェーズ中に同一フェーズのリスナー登録しても実行されない。
※DOMContentLoaded
以外でも同様の方法が使用できる。
複数回呼び出されるリスナーの場合、 onece
指定や登録解除処理が必要になる。
ページ読み込み直後にページの途中を判定する
ウェブページは、基本的に読み込み直後にページ先頭を表示します。ですが、例外的にページの途中を表示することがあります。
次の操作で読み込み直後にページの途中を表示する。
- リロード時
- 例:ページ途中までスクロールした状態でリロードする
- ページ内リンク
- 例:URLにハッシュ(#~:フラグメント識別子)がある
- 履歴・戻る
- 例:戻るボタンでウェブページを戻る
スクロール位置で判定するif (window.performance && !performance.navigation.type && !location.hash) {
// ページ先頭(通常表示のみフラグメント識別子で簡易判定)
} else if (!window.pageYOffset) {
// ページ先頭(スクロール位置で判定)
} else {
// ページ先頭ではない
}
※window.pageYOffset
にアクセスすると Reflow(リフロー)が発生する。
そのため、可能な限りアクセスを回避する。
スクロールイベントで判定する<html>
<script>
var onPageScroll = function() {
if (onPageScroll) {
window.removeEventListener('scroll', onPageScroll);
onPageScroll = 0;
// ページ先頭ではない
}
};
window.addEventListener('scroll', onPageScroll);
window.addEventListener('load', function() {
if (onPageScroll) {
window.removeEventListener('scroll', onPageScroll);
onPageScroll = 0;
// ページ先頭
}
});
</script>
※<head>
内の埋め込みスクリプトで上記処理を実行する必要がある
初回スクロールイベントより先にスクロールリスナーを登録する必要がある
bfcache (Back Forward Cache)
ブラウザでページ遷移した場合、 bfcache に保存します。ブラウザで戻る・進むなどでページ遷移した場合、 bfcache からページをロードします。
bfcache は、ページの状態を JavaScript の実行状態を含めてキャッシュします。
bfcache への状態遷移には、 freeze / resume イベントが発火します。