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

投稿日 2021/04/18

はじめに

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

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

基本

  • <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に直接記述した場合の判定方法です

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イベント中を含む

}

First CPU Idle

<head>
<script>
window.requestIdleCallback(function() {
  // First CPU Idle
});
</script>

requestIdleCallback - Web API | MDN

First Contentful Paint (FCP)

FCP直前 / FCP直後

<head>
<script>
window.requestAnimationFrame(function() {
  // FCP直前



  window.requestAnimationFrame(function() {
    // FCP直後(ただし、描画のフレームがスキップされる可能性がある)

  });
});
</script>

window.requestAnimationFrameは、描画処理の直前に呼び出される
 <head>内の埋め込みスクリプトは、FCP前が保証できる
 よって、次の描画処理はFCPの直前となる
 ただし、描画のフレームがスキップされる可能性がある
Window.requestAnimationFrame() - Web API | MDN

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イベント

ウィンドウ、文書、およびそのリソースがアンロードされる直前に発火する。また、アンロードをキャンセルすることが可能です。

pagehideイベント

他のページを表示する過程において、現在のページを非表示にした時に発火する。

unloadイベント

文書または子リソースがアンロードされるときに発生します。

unloadイベントの処理は、実行が完了することが保証されません。時間のかかる処理は、pagehideイベントで実行するべきです。

備考

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>内の埋め込みスクリプトで上記処理を実行する必要がある
 初回スクロールイベントより先にスクロールリスナーを登録する必要がある

コメント