ハイドレーション詳解

SvelteKitのSSRでは、サーバーで生成されたHTMLがブラウザに送られた後、JavaScriptが「ハイドレーション」という処理を行い、静的なHTMLをインタラクティブなアプリケーションに変換します。このページでは、ハイドレーションの仕組みを詳しく解説します。

この記事で学べること

  • ハイドレーションとは何か、なぜ必要なのか
  • SvelteKitが生成するHTMLの構造とハイドレーションマーカー
  • ハイドレーションの実行フローとタイミング
  • ハイドレーションミスマッチの原因と対策
  • パフォーマンスへの影響と最適化手法
  • Islands Architectureと部分的ハイドレーション

ハイドレーションとは

ハイドレーション(Hydration) とは、サーバーサイドレンダリング(SSR)で生成された静的なHTMLに対して、クライアントサイドのJavaScriptが「水を与えるように」イベントリスナーやリアクティビティを付与し、インタラクティブな状態にする処理です。

なぜハイドレーションが必要か

SSRには大きなメリットがありますが、サーバーで生成されたHTMLだけではインタラクティブな機能が動作しません

状態ボタンクリックフォーム入力状態更新
SSR直後(ハイドレーション前)❌ 反応なし❌ 反応なし❌ 不可
ハイドレーション後✅ 動作✅ 動作✅ リアクティブ

SSRとハイドレーションの関係

ダイアグラムを読み込み中...

ハイドレーションのフロー

SvelteKitにおけるハイドレーションの詳細なフローを見てみましょう。

ダイアグラムを読み込み中...

各フェーズの詳細

1. SSRフェーズ(サーバー側)

// サーバー側で実行される処理のイメージ
// 1. Svelteコンポーネントをサーバー上で実行
const html = render(App, { props: data });

// 2. 状態データをJSON形式でシリアライズ
const serializedData = JSON.stringify({
  type: 'data',
  nodes: [...],  // ページ階層のデータ
});

// 3. HTMLに埋め込んで送信
const fullHtml = `
  <!DOCTYPE html>
  <html>
    <body>
      <div id="app">${html}</div>
      <script id="__sveltekit_data">
        ${serializedData}
      </script>
    </body>
  </html>
`;
typescript

2. 初期表示フェーズ(ブラウザ側)

ブラウザはHTMLを受信すると、JavaScriptの実行を待たずにHTMLをパースして画面に描画します。この時点でユーザーはコンテンツを見ることができますが、ボタンをクリックしても何も起きません。

3. ハイドレーションフェーズ(ブラウザ側)

// SvelteKitランタイムが行う処理のイメージ
// 1. 埋め込みデータを取得
const dataScript = document.getElementById('__sveltekit_data');
const data = JSON.parse(dataScript.textContent);

// 2. 既存のDOMを取得
const target = document.getElementById('app');

// 3. ハイドレーション実行
// - DOMを再生成せず、既存のDOMに「接続」
// - イベントリスナーを付与
// - Runesのリアクティビティを有効化
hydrate(App, { target, props: data });
typescript

SSRで生成されるHTMLの構造

SvelteKitがSSR時に生成するHTMLには、ハイドレーションに必要な情報が埋め込まれています。

ダイアグラムを読み込み中...

実際のHTML出力例

以下は、SvelteKitが生成する典型的なHTMLの構造です。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>ページタイトル</title>

  <!-- JSバンドルの事前読み込み -->
  <link rel="modulepreload" href="/_app/immutable/entry/start.js">
  <link rel="modulepreload" href="/_app/immutable/entry/app.js">
  <link rel="modulepreload" href="/_app/immutable/chunks/scheduler.js">
</head>
<body data-sveltekit-preload-data="hover">
  <!-- SSRで生成されたHTMLコンテンツ -->
  <div id="app" data-sveltekit-hydrate="1a2b3c">
    <header>
      <nav>...</nav>
    </header>
    <main>
      <h1>ようこそ</h1>
      <button>カウント: 0</button>
    </main>
  </div>

  <!-- シリアライズされた状態データ -->
  <script type="application/json" id="__sveltekit_data">
    {
      "type": "data",
      "nodes": [
        { "type": "data", "data": { "count": 0 } },
        { "type": "data", "data": { "title": "ようこそ" } }
      ]
    }
  </script>

  <!-- エントリーポイント -->
  <script type="module">
    import { start } from '/_app/immutable/entry/start.js';
    start();
  </script>
</body>
</html>
html

ハイドレーションマーカーの役割

マーカー役割
data-sveltekit-hydrateハイドレーション対象の要素を識別。値はビルドごとのユニークID
#__sveltekit_dataLoad関数で取得したデータをJSONでシリアライズ
data-sveltekit-preload-dataリンクホバー時のデータプリロード設定
modulepreload重要なJSモジュールを事前に読み込み

ハイドレーションミスマッチ

ハイドレーションミスマッチは、SSRで生成されたHTMLとクライアントで生成されるHTMLが一致しない場合に発生するエラーです。

ダイアグラムを読み込み中...

よくある原因と対策

1. 日時の表示

<!-- ❌ NG: サーバーとクライアントで時刻が異なる -->
<script lang="ts">
  const now = new Date().toLocaleString();
</script>
<p>現在時刻: {now}</p>

<!-- ✅ OK: クライアントでのみ表示 -->
<script lang="ts">
  import { browser } from '$app/environment';

  let now = $state('');

  $effect(() => {
    if (browser) {
      now = new Date().toLocaleString();
    }
  });
</script>
<p>現在時刻: {now || '読み込み中...'}</p>
svelte

2. ランダムな値

<!-- ❌ NG: 毎回異なる値が生成される -->
<script lang="ts">
  const id = Math.random().toString(36).slice(2);
</script>
<div id={id}>...</div>

<!-- ✅ OK: サーバーで生成した値を使う -->
<script lang="ts">
  import type { PageData } from './$types';

  let { data }: { data: PageData } = $props();
</script>
<div id={data.generatedId}>...</div>
svelte
// +page.server.ts
export const load = async () => {
  return {
    generatedId: crypto.randomUUID()
  };
};
typescript

3. ブラウザ専用APIの使用

<!-- ❌ NG: windowはサーバーに存在しない -->
<script lang="ts">
  const width = window.innerWidth;
</script>
<p>画面幅: {width}px</p>

<!-- ✅ OK: browserガードを使用 -->
<script lang="ts">
  import { browser } from '$app/environment';

  let width = $state(0);

  $effect(() => {
    if (browser) {
      width = window.innerWidth;

      const handleResize = () => {
        width = window.innerWidth;
      };
      window.addEventListener('resize', handleResize);

      return () => {
        window.removeEventListener('resize', handleResize);
      };
    }
  });
</script>
<p>画面幅: {width ? `${width}px` : '計測中...'}</p>
svelte

4. 条件付きレンダリング

<!-- ❌ NG: localStorageはサーバーに存在しない -->
<script lang="ts">
  const theme = localStorage.getItem('theme') || 'light';
</script>
{#if theme === 'dark'}
  <DarkTheme />
{:else}
  <LightTheme />
{/if}

<!-- ✅ OK: onMountで初期化 -->
<script lang="ts">
  import { onMount } from 'svelte';

  let theme = $state<'light' | 'dark'>('light');
  let mounted = $state(false);

  onMount(() => {
    theme = (localStorage.getItem('theme') as 'light' | 'dark') || 'light';
    mounted = true;
  });
</script>

{#if !mounted}
  <!-- SSR時のフォールバック -->
  <LightTheme />
{:else if theme === 'dark'}
  <DarkTheme />
{:else}
  <LightTheme />
{/if}
svelte

エラーメッセージの読み方

開発モードでハイドレーションミスマッチが発生すると、以下のような警告が表示されます。

[svelte] hydration_mismatch
Hydration failed because the initial UI does not match what was rendered on the server.
null

デバッグのポイント:

  1. どの要素で発生しているかを特定
  2. サーバーとクライアントで異なる値を生成していないか確認
  3. ブラウザ専用APIを直接使用していないか確認
  4. 条件分岐がサーバーとクライアントで異なる結果になっていないか確認

パフォーマンスへの影響

ハイドレーションはパフォーマンスに大きな影響を与えます。

ページ読み込みのタイムライン

以下の図は、SSRページの読み込みからハイドレーション完了までの典型的なタイムラインを示しています。

ダイアグラムを読み込み中...
  • FCP(First Contentful Paint): HTMLが描画された時点で達成。ユーザーはコンテンツを見られる
  • TTI(Time to Interactive): ハイドレーション完了後に達成。ユーザーが操作できるようになる

重要な指標

指標説明ハイドレーションの影響
FCP (First Contentful Paint)最初のコンテンツが表示されるまでSSRにより改善(ハイドレーション前に表示)
TTI (Time to Interactive)ページがインタラクティブになるまでハイドレーション完了まで待つ必要あり
TBT (Total Blocking Time)メインスレッドがブロックされた時間ハイドレーション中に増加

ハイドレーションのコスト

コンポーネント数が増えると...
├── JSバンドルサイズ増加 → ダウンロード時間増加
├── パース時間増加 → CPUブロック
└── ハイドレーション処理時間増加 → TTI悪化
null

最適化手法

1. コード分割(Code Splitting)

SvelteKitは自動的にルートごとにコード分割を行いますが、大きなコンポーネントは動的インポートで分割できます。

<script lang="ts">
  import type { Component } from 'svelte';

  // 動的インポートでコンポーネントを遅延読み込み
  let HeavyComponent = $state<Component | null>(null);

  // クライアントサイドでのみ読み込み
  $effect(() => {
    import('$lib/components/HeavyComponent.svelte').then((module) => {
      HeavyComponent = module.default;
    });
  });
</script>

{#if HeavyComponent}
  <HeavyComponent />
{:else}
  <div>読み込み中...</div>
{/if}
svelte

または、{#await}ブロックを使用したより簡潔な方法もあります。

{#await import('$lib/components/HeavyComponent.svelte')}
  <div>読み込み中...</div>
{:then { default: HeavyComponent }}
  <HeavyComponent />
{/await}
svelte

2. 遅延ハイドレーション

重要度の低いコンポーネントのハイドレーションを遅延させることで、TTIを改善できます。

<script lang="ts">
  import { browser } from '$app/environment';

  let shouldHydrate = $state(false);

  $effect(() => {
    if (browser) {
      // Intersection Observerで可視になったらハイドレート
      const observer = new IntersectionObserver((entries) => {
        if (entries[0].isIntersecting) {
          shouldHydrate = true;
          observer.disconnect();
        }
      });

      const element = document.getElementById('lazy-section');
      if (element) observer.observe(element);

      return () => observer.disconnect();
    }
  });
</script>

<div id="lazy-section">
  {#if shouldHydrate}
    <InteractiveWidget />
  {:else}
    <!-- 静的なプレースホルダー -->
    <StaticPlaceholder />
  {/if}
</div>
svelte

3. SSGの活用

インタラクティブ性が不要なページは、SSGでプリレンダリングすることでハイドレーションコストを最小化できます。

// +page.ts
export const prerender = true;

// インタラクティブ性が不要ならCSRも無効化
export const csr = false;  // ハイドレーションをスキップ
typescript

Islands Architecture(発展)

Islands Architectureは、ページ全体をハイドレートする代わりに、インタラクティブな「島(Island)」だけを選択的にハイドレートするアーキテクチャパターンです。

ダイアグラムを読み込み中...

従来のハイドレーションとの比較

項目従来のハイドレーションIslands Architecture
ハイドレーション範囲ページ全体必要な部分のみ
JSバンドルサイズ大きい小さい
TTI遅い速い
実装の複雑さシンプルやや複雑
適したサイト高インタラクティブなアプリコンテンツ中心のサイト

Islands Architectureを採用しているフレームワーク

  • Astro: 明示的なIslandsサポート
  • Fresh (Deno): デフォルトでIslands
  • Qwik: Resumability(ハイドレーション不要)

SvelteKitでの実現方法

SvelteKitは標準ではIslands Architectureをサポートしていませんが、以下のアプローチで近い効果を得られます。

アプローチ1: 部分的なCSR無効化

// 静的なページはCSRを無効化
// +page.ts
export const csr = false;  // ハイドレーションをスキップ
export const prerender = true;
typescript

アプローチ2: 動的コンポーネントの遅延読み込み

<!-- 静的な部分 -->
<article>
  <h1>{data.title}</h1>
  <div>{@html data.content}</div>
</article>

<!-- インタラクティブな「島」だけを遅延ハイドレート -->
<div id="comments-island">
  {#await import('$lib/components/Comments.svelte') then { default: Comments }}
    <Comments postId={data.id} />
  {/await}
</div>
svelte

アプローチ3: Web Componentsとの組み合わせ

<script lang="ts">
  import { onMount } from 'svelte';

  onMount(async () => {
    // 必要な時だけWeb Componentを読み込み
    await import('$lib/web-components/interactive-chart.js');
  });
</script>

<!-- 静的コンテンツ -->
<article>
  <h1>売上レポート</h1>
  <p>2024年の売上推移です。</p>
</article>

<!-- Web Componentとして実装されたインタラクティブ要素 -->
<interactive-chart data-src="/api/sales-data"></interactive-chart>
svelte

まとめ

ハイドレーションはSSRの恩恵を受けながらインタラクティブなアプリケーションを構築するための重要な技術です。

押さえておくべきポイント

  1. ハイドレーションの役割: SSRのHTMLにイベントリスナーとリアクティビティを付与
  2. ミスマッチの回避: サーバーとクライアントで同じ出力になるようにする
  3. パフォーマンスへの意識: TTIに影響するため、必要に応じて最適化
  4. 適切な戦略選択: インタラクティブ性が不要なページはSSG + csr: falseを検討

チェックリスト

  • window/document/localStorageを直接使っていないか
  • 日時やランダム値をサーバー/クライアントで別々に生成していないか
  • browserガードやonMountを適切に使用しているか
  • 静的なページで不要なハイドレーションをしていないか

関連ドキュメント

次のステップ

ハイドレーションを理解したら、次はSvelteKitのデータフローをより深く学びましょう。 データロードアーキテクチャ では、Load関数の詳細な動作と最適化について解説します。

Last update at: 2026/01/11 05:19:18