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

はじめに

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

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

基本(script)

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

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

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

Intersection Observer API - Web API | MDN

beforeunload イベント

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

※ページとユーザーの対話が存在しない場合、例え 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 は、現在フォーカスしている要素を返します。
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 イベントが発火します。