SSRで状態を引き渡すDehydrationの正体

SSRのDehydrationは「HTMLを渡す技術」ではなく「状態を渡す技術」です

SSR(Server Side Rendering)という言葉を聞くと、多くの人は「サーバでHTMLを作って返す仕組み」と理解しています。もちろんそれ自体は正しいのですが、実際のSSRの本質はそこではありません。

SSRが本当にやっていることは、画面の状態(state)をサーバからブラウザへ安全に引き渡すことです。その中心にある仕組みが「Dehydration(デハイドレーション)」です。

ReactやNext.jsを使っていると、Hydrationという言葉は頻繁に見かけます。しかし、Hydrationだけを理解してもSSRは理解できません。Hydrationが成立するためには、その前段階で必ずDehydrationが行われています。

この記事では、SSRの仕組みを「描画」ではなく「状態管理」の観点から解説します。

Hydrationの前に必ず存在するDehydration

Hydrationとは何をしている処理なのか

Hydrationは、簡単に言うと「既に存在しているHTMLにJavaScriptの挙動を紐付ける処理」です。

通常のSPAでは、ブラウザは次の順番で画面を作ります。

  • HTMLを取得
  • JavaScriptをダウンロード
  • JavaScriptがDOMを生成
  • 画面が表示

しかしSSRではすでにDOMに相当するHTMLが存在しています。そこでブラウザは「再描画」ではなく「接続」を行います。これがHydrationです。

Reactは次のようなことを内部で行っています。

  • サーバが生成したHTMLを読み取る
  • 仮想DOMを構築する
  • HTMLとの差分を確認する
  • イベントリスナを登録する

つまりHydrationはレンダリングではなく、DOMとアプリケーション状態を同期させる処理です。

なぜそれだけでは動かないのか

ここで重要な疑問が生まれます。

「サーバがHTMLを作ったなら、そのまま動けばいいのでは?」

実際には動きません。理由はシンプルです。ブラウザのJavaScriptはサーバのメモリを知りません。

サーバ側ではすでに次のようなデータが存在しています。

  • APIから取得した商品一覧
  • ログインユーザ情報
  • 検索結果
  • ページング情報

ところがブラウザにはそれがありません。HTMLには見た目しかないからです。

この状態でHydrationを開始すると、Reactはこう判断します。

「データが無いのでAPIを再取得しよう」

すると何が起きるか。

  • 画面が一瞬表示される
  • APIが呼ばれる
  • ローディングになる
  • 再描画される

つまりSSRした意味が消えます。これを防ぐ仕組みがDehydrationです。

Dehydrationの仕組み

サーバはHTMLと一緒に「状態」を送っている

Dehydrationとは、サーバのアプリケーション状態をJSONとしてHTMLに埋め込む処理です。

実際のHTMLには次のようなコードが入っています。

<script id="__NEXT_DATA__" type="application/json">
{
  "props": {
    "pageProps": {
      "posts":[
        {"id":1,"title":"article"}
      ]
    }
  }
}
</script>

このJSONこそがDehydrationされた状態です。

つまりSSRは

  • HTML
  • CSS
  • JavaScript
  • 状態(JSON)

を同時にブラウザへ渡しています。

HydrationはこのJSONを読む

ブラウザのReactは、このJSONを読み取ります。そしてこう判断します。

「すでにデータがある」

その結果、

  • API再取得が起きない
  • ローディングが出ない
  • 初回描画が高速に見える

SSRが「速い」と言われる理由の大半は、実はレンダリング速度ではなく二重フェッチを防いでいることです。

React QueryやSWRで使われるdehydrate関数

データフェッチライブラリではDehydrationが明示的に実装されています。

import { dehydrate, QueryClient } from '@tanstack/react-query'

export async function getServerSideProps() {
  const queryClient = new QueryClient()
  await queryClient.prefetchQuery(['posts'], getPosts)

  return {
    props: {
      dehydratedState: dehydrate(queryClient),
    },
  }
}

ここで行われているのは「キャッシュのシリアライズ」です。

サーバのメモリに存在していたキャッシュをJSONへ変換し、クライアントに渡しています。

そしてブラウザでは

hydrate(queryClient, pageProps.dehydratedState)

が実行され、キャッシュが復元されます。

これがDehydration → Hydrationの完全な流れです。

なぜ「Dehydration」という名前なのか

Hydrationは「水を与える」という意味です。つまり「機能を持たせる」というニュアンスです。

ではDehydrationは何か。

これは「水分を抜く」という意味です。ここでいう水分とはメモリです。

サーバのアプリケーション状態はオブジェクトです。そのままではネットワークを通れません。そこで

  • オブジェクト
  • 関数
  • プロトタイプ

といった情報を削り、純粋なJSONへ変換します。

つまりDehydrationとは

「実行可能な状態を、転送可能な状態へ変換する処理」

です。

SSRで起きがちなトラブルの原因

Hydration mismatchの正体

よくあるエラーに「Hydration mismatch」があります。これはHTMLが違うという意味ではありません。

本質は「状態が違う」です。

例えば

  • 日付をnew Date()で描画
  • Math.random()を使う
  • ユーザエージェント依存の表示

これらはサーバとブラウザで値が変わります。すると

サーバHTML ≠ クライアント仮想DOM

になり、ReactはDOMを作り直します。これがCLS(レイアウトシフト)の原因になります。

セキュリティ上の注意点

Dehydrationは便利ですがリスクもあります。

HTMLにJSONを埋め込むということは、ブラウザから閲覧可能ということです。

次の情報は絶対に入れてはいけません。

  • アクセストークン
  • 内部APIキー
  • 権限フラグの内部情報

実際、SSRアプリの情報漏洩事故の多くはDehydrationが原因です。サーバ変数をそのままpropsに入れてしまうと、view-sourceで見えてしまいます。

結局SSRが高速に見える理由

多くの人が「SSRはサーバで描画するから速い」と考えています。しかし、CPUのレンダリング速度だけなら、現代ブラウザの方が速い場合もあります。

SSRが速く感じる最大の理由は、

  • 初回データ取得が完了した状態で表示される
  • ローディングが存在しない
  • 2回目のAPI通信が発生しない

つまり、SSRの正体はレンダリング技術ではなくデータフェッチ最適化技術です。

そしてその中心にあるのがDehydrationです。

SSRを理解したいなら、テンプレートエンジンやJSXよりも先に「状態の移送」を理解する必要があります。画面はDOMでできていますが、アプリケーションはstateでできています。

SSRはHTMLを送っているのではありません。実行途中のアプリケーションをブラウザに移植しているのです。これを理解すると、Hydrationエラーや二重フェッチの原因が一気に見えるようになります。