SSR/SSG

サーバーサイドレンダリングと静的サイト生成 - Web ページの生成タイミングと場所を制御するレンダリング戦略

Webフロントエンド

SSR/SSG とは

SSR (Server-Side Rendering) と SSG (Static Site Generation) は、ページの HTML をいつ、どこで組み立てるかを決めるレンダリング戦略である。戦略を見分ける軸はこの 2 つで、いつ (ビルド時か、リクエストのたびか) と、どこで (サーバーか、利用者の端末か) の組み合わせが、応答の速さとデータの鮮度を決める。

サーバー側で HTML を組み立てておくと、ブラウザに届いた時点で本文がマークアップに入っている。検索エンジンは JavaScript の実行が必要なページを描画の待ち行列に回すため、HTML に本文が入っていればその待ちを挟まずに済む (この事情は SPA 側で詳しく扱う)。

4 つのレンダリング戦略

戦略いつ生成するかどこで生成するか用途
CSR (Client-Side)ブラウザでの実行時利用者の端末管理画面、検索流入を必要としない画面
SSR (Server-Side)リクエストのたびサーバーダッシュボード、検索結果
SSG (Static Site)ビルド時ビルドを実行するマシンブログ、ドキュメント、LP
ISR (Incremental Static)ビルド時 + 期限切れ後のリクエストを契機に再生成ビルドマシンとサーバーの両方EC サイト、ニュース

4 つは排他ではない。同じアプリケーションの中でページ単位に選べるため、「このサイトは SSG」ではなく「一覧は SSG、注文履歴は SSR」という粒度で決めるのが実際の設計になる。

SSR (Server-Side Rendering)

リクエストを受けてからサーバーでデータを取得し、その場で HTML を組み立てて返す。

// Next.js App Router: Server Component (デフォルトで SSR)
async function ProductPage({ params }: { params: { id: string } }) {
  const product = await db.products.findById(params.id); // サーバーで実行
  return <div><h1>{product.name}</h1><p>{product.description}</p></div>;
}

常に最新のデータを表示できる代わりに、サーバーの処理時間がそのまま応答時間に乗る。データ取得に 300 ミリ秒かかるなら、最初のバイトが届くまでにその分の待ちが加わる。利用者ごとに内容が変わるページは CDN での共有キャッシュも効かないため、遅さがそのまま全リクエストに残る。

SSG (Static Site Generation)

ビルド時に全ページ分の HTML を生成しておき、配信時は出来上がったファイルを返すだけにする。

// Next.js: ビルド時に HTML を生成
export async function generateStaticParams() {
  const posts = await db.posts.findMany();
  return posts.map(post => ({ slug: post.slug }));
}

async function BlogPost({ params }: { params: { slug: string } }) {
  const post = await db.posts.findBySlug(params.slug);
  return <article><h1>{post.title}</h1><div>{post.content}</div></article>;
}

リクエスト時にアプリケーションコードが動かないので、配信は静的ファイルの取り出しに還元でき、S3 + CloudFront のような構成に載せられる。代償は 2 つある。データを更新してもビルドし直すまで古い内容が出続けること、そしてビルド時間がページ数に比例して伸びることである。ページ数が数千から数万の規模になると、1 ページの誤字修正でも全ページ分のビルドを待つことになる。

ISR (Incremental Static Regeneration)

配信形態は SSG のまま、生成し直す契機をビルドからリクエストへ移す仕組みである。

// Next.js: 再生成を 60 秒に 1 回までに制限する
export const revalidate = 60;

async function ProductPage({ params }) {
  const product = await db.products.findById(params.id);
  return <div>{product.name}: ¥{product.price}</div>;
}

revalidate = 60 は「60 秒ごとに再生成する」設定ではなく、「再検証をリクエスト契機で行い、その間隔を 60 秒に 1 回までに制限する」設定である。この違いが挙動に出る。期限が切れた後の最初のリクエストには古い HTML がそのまま返り、その裏側で再生成が走って、次のリクエストから新しい HTML に入れ替わる。したがってアクセスが途絶えている間は期限を過ぎても再生成されず、古さは経過時間ではなくアクセスの有無で決まる。在庫数や価格のように 1 リクエスト分の遅れが実害になる値は、この方式の対象から外して都度取得する。

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

HTML が早く届くことと、押して反応することは別の話である。SSR や SSG で本文が先に表示されても、ブラウザが同じコンポーネントをもう一度実行してイベントハンドラーを結び付ける処理 (ハイドレーション) が終わるまで、ボタンは見えているのに反応しない。

そのため転送するものは減らず、HTML と、同じ画面を組み立て直すための JavaScript の両方を送ることになる。表示が早くなった分だけ、操作できない時間のほうが目に付きやすくなる。この時間を削る方向は 2 つで、クライアントへ JavaScript を送らないコンポーネントを増やすか (Server Components)、対話が必要な部分だけを個別にハイドレートするかである。初回表示の速さと操作できるようになるまでの時間は別々の指標で測る必要があり、LCP が捉えるのは前者だけである。

選択基準

ケース推奨戦略
ブログ、ドキュメントSSG (S3 + CloudFront)
EC サイト (商品ページ)ISR (1 リクエスト分の遅れを許せる項目に限る)
ダッシュボードSSR (常に最新データ)
管理画面CSR (検索流入が不要)

AWS でのホスティング

静的ファイルを配りたいのか、リクエストごとにアプリケーションを動かしたいのかで、必要な部品が変わる。

戦略必要なもの構成例
SSG / CSR静的ファイルの配信S3 + CloudFront
SSRリクエストごとにアプリケーションを実行する環境Amplify Hosting、ECS / FargateLambda
ISR上記 + 再生成した HTML を共有する保存先Amplify Hosting、ECS / Fargate + 共有キャッシュ

ISR で実行環境を複数インスタンスに増やす場合、再生成した HTML をインスタンスの中に抱えると、同じ URL でも到達したインスタンスによって内容が変わる。共有の保存先に置くところまでが構成に含まれる。なお CloudFront に組み合わせる Lambda@Edge や CloudFront Functions は、リクエストとレスポンスに手を入れるための部品であり、フレームワークの SSR をそのまま載せる場所ではない。フレームワーク側の対応状況は Next.js を参照。

SSR/SSG については関連書籍でも詳しく扱われている。

この記事は役に立ちましたか?

関連用語

関連する記事