Back

コードを実行するタイミング:ページロードイベントの解説

コードを実行するタイミング:ページロードイベントの解説

JavaScriptを実際にいつ実行すべきでしょうか?これは、バニラJavaScriptでDOMを操作する場合でも、Reactでコンポーネントのライフサイクルを管理する場合でも、すべてのフロントエンド開発者が直面する問題です。その答えは、ブラウザのページロードイベントを理解し、特定のニーズに適したフックを選択することにかかっています。

重要なポイント

  • DOMContentLoadedはHTMLの解析完了時に発火し、loadはすべてのリソースを待機します
  • モダンブラウザは、信頼性の低いunloadイベントの代わりにPage VisibilityとLifecycle APIを使用します
  • Reactなどのフレームワークは、コンポーネントライフサイクルメソッドを通じてタイミングを処理します
  • document.readyStateをチェックして、既に発火したイベントを見逃さないようにしましょう

従来のブラウザライフサイクルの理解

ブラウザは、ページの読み込み中の特定のタイミングでイベントを発火します。各イベントがいつ発火するか、そしてその時点で何が利用可能かを知ることで、コードをどこに配置すべきかが決まります。

DOMContentLoaded vs load:重要な違い

DOMContentLoadedは、HTMLが完全に解析され、DOMツリーが構築されたときに発火します。画像、スタイルシート、iframeなどの外部リソースはまだ読み込み中です。これは、DOM要素を安全にクエリおよび操作できる最も早い機会です:

document.addEventListener('DOMContentLoaded', () => {
    // DOMの準備は完了していますが、画像はまだ読み込み中の可能性があります
    const button = document.querySelector('#submit');
    button.addEventListener('click', handleSubmit);
});

loadイベントは、すべて(画像、スタイルシート、iframe、その他の外部リソース)を待ちます。完全なリソース情報が必要な場合に使用します:

window.addEventListener('load', () => {
    // すべてのリソースが読み込まれました - 画像のサイズが利用可能です
    const img = document.querySelector('#hero');
    console.log(`Image size: ${img.naturalWidth}x${img.naturalHeight}`);
});

スクリプトがタイミングに与える影響

通常の<script>タグはDOMContentLoadedをブロックします。ブラウザは続行する前にそれらを実行する必要があります。ただし、defer属性を持つスクリプトは、DOM解析後、DOMContentLoadedが発火する前に実行されます。async属性を持つスクリプトは並行して読み込まれ、ダウンロードが完了すると即座に実行されます。これはDOMContentLoadedの前または後になる可能性があります。

要素固有のリソースについては、個別のloadイベントを使用します:

const img = new Image();
img.addEventListener('load', () => console.log('Image ready'));
img.src = 'photo.jpg';

モダンなアプローチ:document.readyStateとその先

document.readyStateの使用

イベントリスナーが間に合うことを期待するのではなく、現在の状態を確認します:

function initialize() {
    // 初期化コード
}

if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', initialize);
} else {
    // DOMContentLoadedは既に発火済み
    initialize();
}

3つの状態は以下の通りです:

  • 'loading' - ドキュメントを解析中
  • 'interactive' - 解析完了、DOMContentLoadedが発火直前
  • 'complete' - すべてのリソースが読み込まれました

jQueryの$(document).ready()は、本質的にこのパターンのクロスブラウザラッパーでした。現在では、ネイティブAPIがブラウザ間で確実にこれを処理します。

Page VisibilityとLifecycle API:新しい現実

beforeunloadとunloadが問題である理由

モダンブラウザは、バックフォワードキャッシュ(bfcache)などの機能を通じて、パフォーマンスを積極的に最適化します。bfcacheに入るページはアンロードされず、一時停止されます。これは、ユーザーが離れるときにbeforeunloadとunloadイベントが確実に発火しないことを意味します。さらに、ブラウザは現在、悪用を防ぐためにbeforeunloadダイアログのカスタムメッセージを制限または無視しています。

Page Visibility APIの代替手段

信頼性の低いunloadイベントの代わりに、Page Visibility APIを使用して、ユーザーがタブを切り替えたりブラウザを最小化したりしたときに応答します:

document.addEventListener('visibilitychange', () => {
    if (document.hidden) {
        // ページが非表示 - 高コストな操作を一時停止
        pauseVideoPlayback();
        throttleWebSocket();
    } else {
        // ページが再び表示されました
        resumeOperations();
    }
});

Page Lifecycleの状態

Page Lifecycle APIは、frozen(メモリを節約するためにタブが一時停止)やterminatedなどの状態でこれを拡張します:

document.addEventListener('freeze', () => {
    // 状態を保存 - タブが破棄される可能性があります
    localStorage.setItem('appState', JSON.stringify(state));
});

document.addEventListener('resume', () => {
    // フリーズ後に復元
    hydrateState();
});

React useEffect vs DOMContentLoaded

Reactやその他のモダンフレームワークでは、DOMContentLoadedを直接使用することはほとんどありません。コンポーネントライフサイクルメソッドが初期化のタイミングを処理します:

// Reactコンポーネント
import { useEffect } from 'react';

function MyComponent() {
    useEffect(() => {
        // コンポーネントのマウント後とDOM更新後に実行されます
        // このコンポーネントのDOMContentLoadedと同様のタイミング
        initializeThirdPartyLibrary();
        
        return () => {
            // アンマウント時のクリーンアップ
            cleanup();
        };
    }, []); // 空の依存配列 = マウント後に1回実行
    
    return <div>Component content</div>;
}

Next.jsやその他のSSRフレームワークの場合、useEffect内のコードはハイドレーション後にクライアント側でのみ実行されます。フレームワークがサーバー対クライアント実行タイミングの複雑さを処理します。

適切なフックの選択

バニラJavaScriptの場合:

  • DOM操作:DOMContentLoadedを使用
  • リソース依存コード:window loadイベントを使用
  • 状態の永続化:Page Visibility APIを使用
  • 現在の状態の確認:document.readyStateを使用

SPAとフレームワークの場合:

  • コンポーネントの初期化:フレームワークライフサイクル(useEffect、mountedなど)を使用
  • ルート変更:ルーターイベントを使用
  • バックグラウンド/フォアグラウンド:引き続きPage Visibility APIを使用

避けるべきパターン:

  • 重要な操作にbeforeunload/unloadを使用しない
  • ReactコンポーネントでDOMContentLoadedを使用しない
  • スクリプトが新しいページ読み込みで実行されると仮定しない(bfcacheを考慮)

結論

JavaScriptのページロードイベントは、単純なDOMContentLoadedとloadハンドラーを超えて進化しました。これらの従来のイベントはバニラJavaScriptにとって依然として不可欠ですが、モダンな開発にはPage Visibility、Lifecycle API、フレームワーク固有のパターンの理解が必要です。必要なリソースと、サーバーレンダリングされたページで作業しているのか、完全なSPAを構築しているのかに基づいて、初期化戦略を選択してください。最も重要なのは、unloadイベントのような非推奨のパターンに依存せず、現代のWebアプリケーション向けに構築されたモダンなAPIを採用することです。

よくある質問

deferスクリプトは、DOM解析後、DOMContentLoadedの前に順番に実行されます。asyncスクリプトはダウンロード時に即座に実行され、解析を中断する可能性があります。DOMアクセスが必要なスクリプトにはdeferを、アナリティクスのような独立したスクリプトにはasyncを使用してください。

window.onloadは機能しますが、addEventListenerが推奨されます。複数のハンドラーを許可し、既存のものを上書きしないためです。loadイベント自体は、コードを実行する前にすべてのリソースが完全に読み込まれる必要がある場合に依然として有用です。

クライアント側の初期化には、空の依存配列を持つuseEffectを使用します。これはハイドレーション完了後に実行されます。サーバー側のコードには、ブラウザイベントの代わりにgetServerSidePropsやgetStaticPropsなどのフレームワーク固有のメソッドを使用してください。

モダンブラウザはページをキャッシュし、ユーザーが離れるときにbeforeunloadを発火しない可能性があります。代わりにPage Visibility APIを使用してユーザーが離れたことを検出し、終了時ではなく継続的に重要なデータを永続化してください。

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay