JavaScriptのライフサイクルに関するイベント

はじめに

ここでは、ウェブページのライフサクル(ページの読み込みから開放まで)に関するJavaScriptのイベント(実行タイミング)の覚書です。

onLazy.jsの開発で得られた知見をまとめて記載します。

基本(script)

  • <script>を発見した場合の処理
    • type属性がtext/javascript以外の場合、スクリプトは実行されない
      • type属性未定義は、text/javascriptとして解釈される
    • async属性がある場合、スクリプトの読み込みと実行を非同期で実行する
      • HTMLの解析処理は、スクリプトの処理と平行して継続する
      • スクリプトは、スクリプトの読み込み後に実行する
        • HTMLの解析処理中に実行されることがある
        • HTMLの解析処理完了後に実行されることもある
        • async属性のある<script>の実行順は、保証されない
      • src属性が必須
    • defer属性がある場合、スクリプトの読み込みを非同期で実行する
      • HTMLの解析処理は、スクリプトの読み込みと平行して継続する
      • HTMLの解析完了後にスクリプトを実行する
        • defer属性のある<script>の実行順は、保証される
      • async属性と併記した場合、ブラウザが対応していればasync属性として処理する
      • src属性が必須
    • src属性がある場合、ファイルの読み込みを待機して、スクリプトを実行する
      • スクリプト実行後にHTMLの解析処理を再開する
      • async/defer属性がある場合、async/defer属性として処理される
    • 上記以外の場合、HTMLの解析処理を中断する(タスクが分割される)
      • スクリプトを即座に実行する

※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イベントは、persistedtrueが設定されています。

初回以降にページがロードされた場合、pageshowイベントはpersistedfalseが設定されています。初回以外のページロードは、戻る機能などでページがキャッシュされていた場合に発生します。(ページがキャッシュされていた場合、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);

Intersection Observer API - Web API | MDN

beforeunloadイベント

ページががアンロードされる直前に発火します。そして、ダイアログを表示してページ遷移をキャンセルするか確認できます。

※ページとユーザーの対話が存在しない場合、例えbeforeunloadイベントを設定していてもダイアログは表示されません。これは、ユーザーとの対話が存在しない場合、ページ上から失われるデータが存在しないため、ページ遷移をキャンセルする理由そのものがなくなるためです。
 beforeunload を設定したページをクリックあり・なしでクローズすることでこの動作を確認できます。

pagehideイベント

pagehideイベントは、unloadイベントと類似しています。

初回のpagehideイベントは、unloadイベントの発動直前に発火します。初回のpagehideイベントは、persistedtrueが設定されています。

初回以降にページがアンロードされた場合、pagehideイベントはpersistedfalseが設定されています。初回以外のページのアンロードは、戻る機能などでページがキャッシュされていた場合に発生します。

※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 イベントが発火します。