JavaScriptのライフサイクルに関するイベント
はじめに
ここでは、ウェブページのライフサクル(ページの読み込みから開放まで)に関するJavaScriptのイベント(実行タイミング)の覚書です。
onLazy.jsの開発で得られた知見をまとめて記載します。
基本(script)
<script>
を発見した場合の処理- type属性が
text/javascript
以外の場合、スクリプトは実行されない- type属性未定義は、
text/javascript
として解釈される
- type属性未定義は、
- async属性がある場合、スクリプトの読み込みと実行を非同期で実行する
- HTMLの解析処理は、スクリプトの処理と平行して継続する
- スクリプトは、スクリプトの読み込み後に実行する
- HTMLの解析処理中に実行されることがある
- HTMLの解析処理完了後に実行されることもある
- async属性のある
<script>
の実行順は、保証されない
- src属性が必須
- defer属性がある場合、スクリプトの読み込みを非同期で実行する
- HTMLの解析処理は、スクリプトの読み込みと平行して継続する
- HTMLの解析完了後にスクリプトを実行する
- defer属性のある
<script>
の実行順は、保証される
- defer属性のある
- async属性と併記した場合、ブラウザが対応していればasync属性として処理する
- src属性が必須
- src属性がある場合、ファイルの読み込みを待機して、スクリプトを実行する
- スクリプト実行後にHTMLの解析処理を再開する
- async/defer属性がある場合、async/defer属性として処理される
- 上記以外の場合、HTMLの解析処理を中断する(タスクが分割される)
- スクリプトを即座に実行する
- type属性が
※async/deferについて、次の記事が参考になります
<script> タグに async / defer を付けた場合のタイミング - Qiita
※module/module async
属性がある
実行順だけに着目すれば、defer/async
属性と同じ
スタイルシートによるスクリプトのブロック
外部ファイルのスタイルシート宣言が先にある場合、スタイルシートの読み込み完了まで<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に直接記述した場合の判定方法です
基本(状態遷移)
次の記事が参考になります。
Page Lifecycle API | Web | Google Developers
readystatechangeイベント
document.readyState
の値変更時に発火します。
document.readyState
は、loading/interactive/complete
の3つの値に遷移します。readystatechangeイベントは、interactive/complete
変更時の合計2回呼び出されます。interactive
変更時は、HTMLの解析完了直後でdefer属性のスクリプトの実行直前です。その後、DOMContentLoadedイベントが実行されます。complete
変更時は、loadイベントの実行直前です。
※document.readyState
の値は、readystatechangeイベント直前に変更されます。
DOMContentLoadedイベント
最初の HTML 文書の読み込みと解析が完了時に発火する。
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イベントは発生しません)
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直後とは限らない)
}).observe({type:'paint', buffered:true});
※PerformanceObserver() - Web API | MDN
※Largest Contentful Paint (LCP) も同様に検出できる
First Input Delay (FID)
FID後
new PerformanceObserver((entryList) => {
// FID後(ただし、FID直後とは限らない)
}).observe({type: 'first-input', buffered: true});
初回ユーザイベント
click/mousedown/keydown/touchstart/mousemove/scroll
などのイベントをページ読み込み後に初めて取得したタイミング
onLazy.jsの主たる機能の1つ。
※FIDで使用されるユーザイベントは、click/mousedown/keydown/touchstart/pointerdown
です。
初回スクロール
scroll
イベントをページ読み込み後に初めて取得したタイミング
onLazy.jsの主たる機能の1つ。
対象要素が表示領域内に入ったタイミング
let options = {
root: document.querySelector('#scrollArea'),
rootMargin: '0px',
threshold: 1.0
}
let observer = new IntersectionObserver(callback, options);
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
は、現在フォーカスしている要素を返します。
※CORS制限付き外部iframeのfocusイベントを取得する
※document.hasFocus()
は、ドキュメントのフォーカスを判定できます。
備考
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 イベントが発火します。