定義
ログインが必要なページで、画面が出た瞬間にデータがすでに埋まって見えるようにしたいときに使うパターンだ。ブラウザが開いてからデータを要求すると、ローディングスピナーが一拍遅れて消えるが、そのデータをサーバーが先に受け取って HTML に載せて送る方式でなくす。
かみ砕くと:サーバーで必要な API を先に呼び(prefetch)、その結果をクライアントデータライブラリ(React Query・SWR など — サーバーデータのキャッシュ・再要求を管理してくれるツール)のキャッシュに入れて渡す。このときサーバーキャッシュを文字列にシリアライズして HTML に埋め込むことを dehydrate、ブラウザがそれを復元してキャッシュを埋めることを hydrate という。すると初回レンダーからキャッシュにデータがありスピナーが出ない。ただし、キャッシュを引く鍵である query key(例:["items", "page=1&size=50"] — どのリクエストの結果か区別する配列)がサーバーとクライアントで完全に同じでないと cache hit にならない。
なぜ必要か
ログインが必要なアプリのホームで、画面・主要画像が API 結果に縛られると、ブラウザだけでは「まずセッションを確認して → その後データを受け取る」waterfall(リクエストが前後に連なって画面が遅れて出る現象)が生じる。ここで LCP(Largest Contentful Paint — 画面で最も大きい要素が描画される時刻、体感ロード速度の指標)がそのデータに足を引っ張られる。サーバーローダーで Promise.all([fetchSession(), fetchList(query)]) により並列呼び出しし、HTML 応答とともにキャッシュを渡せばロード遅延を減らせる。逆に query key が食い違うと prefetch が無効になり、遅いスケルトン(ローディング用のプレースホルダー UI)と CLS(Cumulative Layout Shift — 遅れてコンテンツが入りレイアウトが押される度合い)が再び現れることがある。
動作原理
- Server-only fetch —
server-onlyモジュールに API クライアントを分離。 - Shared query keys —
queryKeys.list(serializedParams)をサーバー・クライアントが同一 import。 - Loader — 認証失敗時は redirect;成功時は prefetch + dehydrate。
- Client boundary —
HydrationBoundaryでキャッシュを注入、下位のuseQueryが即座にdataを保持。 - Loading UI —
isPending && rows.length === 0のときだけ skeleton(cls-skeleton-layout-reservation)。
クライアント TableProvider のデフォルト page/pageSize/filter が buildQueryString とバイト単位で同一でなければならない。
実務適用
- LCP 画像が API URL に依存する場合:サーバーで auth + metadata(最初の画像 URL)だけを prefetch;すべてのバイトを SSR に入れる必要はない(carousel-viewport-image-deferral)。
- TTFB は prefetch で増えうる — 目標が client waterfall の除去・load delay の短縮なのかを明確にする。
- Streaming SSR は HTML flush 時点の軸だ。LCP-critical を後の Suspense に先送りすると FCP だけが上がり LCP はそのままのことがある(streaming-ssr-metric-impact)。
- E2E: storageState でログインを維持したうえで trace を再現。
トレードオフ
| 選択 | 利得 | コスト |
|---|---|---|
| Server parallel prefetch | 初期 data・LCP delay ↓ | TTFB・サーバー作業 ↑ |
| Client-only fetch | サーバーがシンプル | waterfall・遅い paint |
| Aggressive prefetch 全部 | cache hit が高い | 不要データ・シリアライズコスト |
使ってはいけない場合
- 公開静的ページ(認証・パーソナライズなし)— CDN/SSG だけで十分な場合。
- query param がクライアントでのみ決まり、サーバーが知り得ない場合(キー整合が不可)。
よくある間違い
- サーバー
page=1&size=50、クライアントpage_size=50などのシリアライズ不一致。 isLoadingだけを見て hydrate されたdataを無視する。- prefetch を追加した後 skeleton 設計を調整せず → CLS 回帰。
関連概念
- ssr-hydration-mismatch — サーバー HTML vs 初回レンダー
- largest-contentful-paint — auth-gated LCP
- cls-skeleton-layout-reservation — prefetch + skeleton のセット
- carousel-viewport-image-deferral — メタだけ prefetch、バイトは defer
- streaming-ssr-metric-impact — streaming flush vs LCP チャンク配置