Closed8

【Next.js】CSR/SSR/SSG/ISRの違いについて

橋田至橋田至

CSR(Client-Side Rendering)

CSRはクライアントからのリクエストに対して、サーバーは空または最小限のHTMLとJavaScriptを返します。
このJavaScriptがブラウザ上で実行されることにより、実際に表示するHTMlをレンダリングする。

App Routerの場合はクライアントコンポーネントに該当する

CSRコード例

コード例ではuseEffectを使用してデータフェッチをしているが、SWRやTanstack Queryなどのデータフェッチライブラリを使用することを推奨
"use client"を明示的に指定することでクライアントコンポーネントになる

index.tsx
"use client";
import React, { useState, useEffect } from 'react'
 
export function Page() {
  const [data, setData] = useState(null)
 
  useEffect(() => {
    const fetchData = async () => {
      const response = await fetch('https://api.example.com/data')
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`)
      }
      const result = await response.json()
      setData(result)
    }
 
    fetchData().catch((e) => {
      // handle the error as needed
      console.error('An error occurred while fetching the data: ', e)
    })
  }, [])
 
  return <p>{data ? `Your data: ${data}` : 'Loading...'}</p>
}
  • レンダリングの流れ:
  1. 初期のHTMLが空または最小限のものでサーバーから送信される。
  2. ブラウザがHTMLを読み込んでJSファイルを取得する。
  3. JavaScriptが実行されて初めてページがレンダリングされる。

レンダリングの例

  1. 初期のHTML例
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>CSR Page</title>
</head>
<body>
    <!-- 空または最小限のhtmlが返される -->
    <div id="__next">
    <p>'Loading...'</p>
    </div> 
    <script defer src="/_next/static/chunks/main.js"></script>
    <script defer src="/_next/static/chunks/pages/_app.js"></script>
    <script defer src="/_next/static/chunks/pages/csr-example.js"></script>
</body>
</html>
  1. JavaScriptが実行された後にレンダリングされたHTML
    JavaScriptがクライアント側で実行されると、データフェッチ後のコンテンツが描画される
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>CSR Page</title>
</head>
<body>
    <div id="__next">
        <p>Your data: [取得したデータ]</p>
    </div>
    <script defer src="/_next/static/chunks/main.js"></script>
    <script defer src="/_next/static/chunks/pages/_app.js"></script>
    <script defer src="/_next/static/chunks/pages/csr-example.js"></script>
</body>
</html>

この理由から以下のデメリットが存在する

  • 初回のコンテンツ表示が遅い: クライアント側でJavaScriptが実行されるまでコンテンツが見えないため、初回表示速度が遅くなる。
    • JavaScriptはブラウザで実行されるため、ページ表示までの時間(JavaScriptの実行時間)がユーザーのマシンスペックに依存してしまう
  • SEO: 空のHTMLがサーバーから返されるため、検索エンジンがクライアント側でレンダリングされるコンテンツを完全にインデックスしにくい。
    • クローラーがJavaScriptを実行しない場合、重要な商品情報を見逃す可能性がある
    • 最新のGoogleボットはJavaScriptを実行できるようだが、インデックス化に時間がかかる場合がある
橋田至橋田至

SSR(Server-Side Rendering)

SSRは、サーバー側でJavaScriptを実行して表示するHTMLを生成(レンダリング)し、そのHTMLをクライアントに送信する方法です。クライアントは初期ロード時にすぐにレンダリングされたHTMLを表示します。

Pages RouterではgetServerSidePropsを使用していたが、App Routerではサーバーコンポーネントが該当する

コード例: デフォルトでサーバーコンポーネントになる

index.tsx
import React from 'react';

// サーバーコンポーネントとしての関数
export default async function Page() {
  // データをサーバー側で取得する
  const data = await fetch('https://jsonplaceholder.typicode.com/posts/1')
    .then(res => res.json());

  return (
    <div>
      <h1>{data.title}</h1>
      <p>{data.body}</p>
    </div>
  );
}

  • レンダリングの流れ:
  1. ユーザーがリクエストを送信。
  2. サーバーがそのリクエストに応じてページをレンダリングし、完全なHTMLを生成。
  3. レンダリングされたHTMLがブラウザに送信され、即座に表示される。
  4. その後、JavaScriptがロードされ、クライアント側でReactが再度起動してインタラクティブ性を提供する。(ハイドレーション)

サーバーが返すhtml

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Next.js Page</title>
</head>
<body>
  <div id="__next">
    <div>
      <h1>データ取得後</h1>
      <p>hello world!</p>
    </div>
  </div>
</body>
</html>

このため、SSRではCSRに比べて以下のメリットが存在する

  • SEOに強い: 初回ロード時に完全なHTMLを提供できるため、検索エンジンがコンテンツをインデックスしやすい。
  • 初回のコンテンツ表示が早い: ユーザーはサーバーから受け取ったHTMLをすぐに見ることができるため、初回表示速度が速い。
    • ブラウザで実行されるJavaScriptを最小限にできるため、ユーザーのマシンスペックに依存することなく、安定した表示速度を提供できる
橋田至橋田至

SSG(Static Site Generation)

SSGは、ビルド時にページを静的なHTMLファイルとして生成し、その後はサーバーに負担をかけずに配信するレンダリング手法です。ユーザーはサーバーリクエストの遅延なしにすぐに静的コンテンツを受け取れる。

App routerでSSGを使用するにはgenerateStaticParamsを使用する。

実装例

app/blog/[id]/page.tsx
interface Post {
  id: string
  title: string
  content: string
}
 
export async function generateStaticParams() {
  const posts: Post[] = await fetch('https://api.vercel.app/blog').then((res) =>
    res.json()
  )
  return posts.map((post) => ({
    id: String(post.id),
  }))
}
 
export default async function Page({ params }: { params: { id: string } }) {
  const post: Post = await fetch(
    `https://api.vercel.app/blog/${params.id}`
  ).then((res) => res.json())
  return (
    <main>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </main>
  )
}

注意点としてSSGでは静的なHTMLファイルを配信するため、ページを更新するには再ビルドが必要になる。
つまりAPIなどでコンテンツの内容を更新した場合でも、ページの内容が変わりません。

これを解決するためには後述するISRを使用する

メリット:

  • 高速なパフォーマンス: ビルド時に生成されたHTMLファイルを配信するため、非常に速いロード時間を実現。
  • SEOに強い: 生成されたHTMLは検索エンジンに容易にインデックスされます。
  • サーバー負荷が低い: 静的ファイルを配信するため、サーバーリソースが少なく済む。

デメリット:

  • 更新頻度の制限: ページを更新するには再ビルドが必要なため、頻繁な更新が求められるコンテンツには不向き。

ちなみにそもそもコンポーネント内でデータフェッチを行っていない場合はデフォルトでSSGとして認識される。

index.tsx
import React from "react";

const SsgPage = async () => {
  return (
    <>
      <h1>SSG Build</h1>
    </>
  );
};

export default SsgPage;
橋田至橋田至

ISR(Incremental Static Regeneration)

ISRは、SSGの弱点を補うために導入された手法で、ページを動的に再生成して最新のコンテンツを提供しつつ、静的生成の利点を維持します。

ページを動的に再生成するタイミングを指定する方法には以下の3つが存在します

  • revalidate(時間ベースの再検証)
  • revalidatePath(パスごとの再検証)
  • revalidateTag(タグごとの再検証)

実装例

時間ベースの再検証

実装方法としてはgenerateStaticParamsrevalidateオプションを使用して、ページが再生成される頻度を指定する。

export const revalidate = 3600を指定することで一定秒数以降にリクエストがきた時に、サーバー側でデータフェッチを再度行い、HTMLが再構築される。

app/blog/[id]/page.tsx
interface Post {
  id: string
  title: string
  content: string
}
 
export const revalidate = 3600 // invalidate every hour
 
export default async function Page() {
  const data = await fetch('https://api.vercel.app/blog')
  const posts: Post[] = await data.json()
  return (
    <main>
      <h1>Blog Posts</h1>
      <ul>
        {posts.map((post) => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
    </main>
  )
}

revalidatePathによる再検証

より正確な再検証を行うには、 revalidatePathを使用してオンデマンドでページを更新できる。

これを使用すると、新しい投稿を追加した際に呼び出すことが可能になるため、古い情報が表示されることを減らせます。

適応する場面はAPIリクエストが発生する2つ

  • Server Action
  • Route Handler
app/actions.ts
'use server'
 
import { revalidatePath } from 'next/cache'
 
export async function createPost() {
  // Invalidate the /posts route in the cache
  revalidatePath('/posts')
}

revalidatePathによる再検証

より細かい制御が必要な場合はrevalidateTagを使用して個々のfetchなどにタグを付けることで再検証をトリガーします。

app/blog/page.tsx
export default async function Page() {
  const data = await fetch('https://api.vercel.app/blog', {
    next: { tags: ['posts'] },
  })
  const posts = await data.json()
  // ...
}

メリット:

  • パフォーマンスと最新性の両立: SSGと同じパフォーマンスを提供しつつ、指定したタイミングごとに最新コンテンツを配信可能。
  • スケーラブル: リクエストごとにサーバーサイドで再生成するSSRよりもサーバーに対する負荷が低い。
  • SEO: 生成済みの静的なHTMLを配信するため、SEOの問題がない。

デメリット:

  • ラグ: ページの更新は指定したタイミングでの更新になるため、最新ではない情報が表示される可能性がある。
橋田至橋田至

ユースケース

  • CSR/SSRはSNSなど最新のコンテンツを常に表示したいサイトの構築に適している。
  • SSG/ISRはブログやコーポレートサイトなど、頻繁に更新されないサイトに適している。
橋田至橋田至

ちなみに

CSR/SSRの比較をする際によく混同されるSPA/MPAについて

アプリケーションとしての違い

  • MPAはページ遷移のたびに新しいHTMLをサーバーから取得する
  • SPAはページ遷移時にJavaScriptで画面の差分を描画する

MPA(Multi-Page Application)

MPAは複数のページから構成されるウェブアプリケーションの形式。
ユーザーが異なるリンクをクリックすると、そのたびに新しいページがロードされ、画面が切り替わる。
例えば、トップページから「お問い合わせ」ページへのリンクをクリックすると、新しいページに遷移してフォームが表示されるような仕組みのこと。

Next.jsでもnext/link を利用せずに<a>タグを使用した場合にはクライアント側でのルーティングとならず、サーバー側に HTML ファイルを要求する
そのため、next/link を利用しない場合は、クライアント側でのルーティングが行われず、画面遷移のたびにサーバー側に HTML ファイルを要求するマルチページアプリケーション (MPA) となる

SPA(Single-Page Application)

SPAは1つのメインページで構成されるウェブアプリケーションの形式。
初回のアクセス時に必要なリソース(HTML、CSS、JavaScript)をまとめて読み込み、その後はページ遷移時のサーバー通信を最小限にして非同期的にコンテンツをロードし、表示を更新する。

ユーザーが異なる部分をクリックしても、ページ全体のリロードは行われず、必要な部分だけが更新される。

Next.jsではnext/link を使用することでクライアントサイド遷移を実現し、ページ遷移時に部分的なコンテンツ更新が行われる。

橋田至橋田至

レンダリングの違いについて

レンダリングを簡単に説明すると、データを取得してHTMLに組み込むプロセスのこと。

JSフレームワークは、データの取得やビルドなどの工程を経て、最終的に静的なHTML、CSS、JavaScriptを生成し、それをブラウザが表示します。この静的ファイルがどこで生成されるかによって、CSR、SSR、SSGの、ISRが存在する。

このスクラップは23日前にクローズされました