Next.jsのレンダリングについてまとめました【SSR/SSG/CSR/ISR/TypeScript】

JavaScript

Next.jsはページごとにレンダリング方法を設定することができます。
レンダリングの種類と挙動について調べたのでメモしておきます。

勉強中なので変なところがあったら直します、、!

Next.jsのレンダリングの種類

Next.jsには、主に次のようなレンダリング方法があります。

  • SSR(サーバーサイドレンダリング)
  • SSG(スタティックサイトジェネレーション)
  • CSR(クライアントサイドレンダリング)
  • ISR(インクリメンタル静的再生成)

Next.jsでは通常、HTMLを事前に生成しますが、上記のレンダリングの違いは、いつ、どこでHTMLを生成するかの違いとなります。

いつは、主にクライアントからのアクセスやアプリをビルドした時
どこでは、サーバー側かクライアント側です。

SSRではサーバー側でアクセスごとにHTMLを生成し、SSGではビルド時にHTMLを生成します。ISRはビルド時にHTMLを生成し、アクセスがあれば改めてHTMLを再生成します。
CSRではSSR、SSG、ISRがサーバー側でHTMLを生成したのに対し、クライアント側でHTMLを生成します。

これだけだと全然わからないので詳しくみていきます!

参考ページ(公式)

Basic Features: Pages | Next.js
Next.js pagesとはpagesディレクトリ内のファイルからエクスポートされたReactコンポーネントです。どのように動作するか学んでいきましょう。

レンダリング検証用サンプル

レンダリングページを種類ごとに再現したページをVercelにデプロイしました。動作はこちらで確認できます。このページで挙動を検証してみます!

コードはGitHubで確認できます。
TypeScriptなどレンダリングに直接関係ないコードはこちらで確認できます。

GitHub - matsu0314/nextjs_rendering
Contribute to matsu0314/nextjs_rendering development by creating an account on GitHub.

それぞれのページにHTMLを生成した時間を表示するようにしました。
レンダリングの種類によっての挙動の違いを確認できます。

ビルドした時のレンダリングの種類の表示

Next.jsはnpm run buildでビルドした時、ページごとにレンダリングの種類を教えてくれます。
ここで想定外のレンダリングになっていたらコードを見直します。

Route (pages)                              Size     First Load JS
┌ ○ /                                      545 B          76.1 kB
├   /_app                                  0 B            73.1 kB
├ ○ /404                                   182 B          73.3 kB
├ ○ /csr                                   899 B          76.4 kB
├ ● /isr (ISR: 5 Seconds)                  807 B          76.3 kB
├ ● /ssg                                   804 B          76.3 kB
└ λ /ssr                                   807 B          76.3 kB
+ First Load JS shared by all              73.3 kB
  ├ chunks/framework-2c79e2a64abdb08b.js   45.2 kB
  ├ chunks/main-8f223987a60cd3fc.js        26.8 kB
  ├ chunks/pages/_app-5fbdfbcdfb555d2f.js  296 B
  ├ chunks/webpack-8fa1640cc84ba8fe.js     750 B
  └ css/028c3f1eeeabc2f7.css               148 B

λ  (Server)  server-side renders at runtime (uses getInitialProps or getServerSideProps)
○  (Static)  automatically rendered as static HTML (uses no initial props)
●  (SSG)     automatically generated as static HTML + JSON (uses getStaticProps)
      (ISR)     incremental static regeneration (uses revalidate in getStaticProps)

ページ名の冒頭についているマークのλはSSR、 はSSGとISR、はPropsや外部データがない静的ページを表します。CSRはクライアント側で外部データを取得するので静的ページの扱いです。

開発モードでの動作確認

開発モードnpm run devで実行した場合、本番にデプロイした時とレンダリングの挙動が異なります。
また、今回はAPIのURL(エンドポイント)を環境変数としてVercel側に設定しています。環境変数の扱いも本番と開発モードでは異なるので、開発モードでの動作確認はCodeSandboxを参考にします。

https://codesandbox.io/p/github/matsu0314/nextjs_rendering/main?layout=%257B%2522activeFilepath%2522%253A%2522%252F.env.local%2522%252C%2522openFiles%2522%253A%255B%2522%252FREADME.md%2522%252C%2522%252F.env.local%2522%255D%252C%2522sidebarPanel%2522%253A%2522EXPLORER%2522%252C%2522gitSidebarPanel%2522%253A%2522COMMIT%2522%252C%2522fullScreenDevtools%2522%253Afalse%252C%2522rootPanelGroup%2522%253A%257B%2522direction%2522%253A%2522vertical%2522%252C%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522id%2522%253A%2522DEVTOOLS_PANELS%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL%2522%252C%2522panelType%2522%253A%2522TABS%2522%252C%2522id%2522%253A%2522clhbc2x5l00093b6nebgc0bhq%2522%257D%255D%252C%2522sizes%2522%253A%255B100%255D%257D%252C%2522tabbedPanels%2522%253A%257B%2522clhbc2x5l00093b6nebgc0bhq%2522%253A%257B%2522id%2522%253A%2522clhbc2x5l00093b6nebgc0bhq%2522%252C%2522activeTabId%2522%253A%2522clhbc37mj00bx3b6ny417oa3x%2522%252C%2522tabs%2522%253A%255B%257B%2522type%2522%253A%2522TASK_LOG%2522%252C%2522taskId%2522%253A%2522dev%2522%252C%2522id%2522%253A%2522clhbc35o9005y3b6ncgwwcmgd%2522%257D%252C%257B%2522type%2522%253A%2522TASK_PORT%2522%252C%2522taskId%2522%253A%2522dev%2522%252C%2522port%2522%253A3000%252C%2522id%2522%253A%2522clhbc37mj00bx3b6ny417oa3x%2522%252C%2522path%2522%253A%2522%252Fisr%2522%257D%255D%257D%257D%252C%2522showSidebar%2522%253Atrue%252C%2522showDevtools%2522%253Atrue%252C%2522sidebarPanelSize%2522%253A15%252C%2522editorPanelSize%2522%253A48.065296251511484%252C%2522devtoolsPanelSize%2522%253A35%257D

開発モード(npm run dev)だと、レンダリングの種類に関係なくリクエストごとにコードが実行されるので注意が必要です。

外部API

テスト用の外部APIとしてJSONPlaceholderを使用します。JSONPlaceholderはダミーのAPIサービスです。エンドポイントに対してHTTPリクエストを送信することで、ダミーデータを受け取ることができます。

今回、こちらのhttps://jsonplaceholder.typicode.com/usersのjsonデータを使用します。
次のようなユーザーの情報が10件あります。ユーザー情報の中でもid、name、username、emailを使ってユーザー情報のテーブルを表示させます。

SSR(サーバーサイドレンダリング)

SSR(Server Side Rendering)は、ユーザーがサイトにアクセスした時、サーバー側で生成したHTMLを返します。その際、PropsやAPIから取得したデータを埋め込んだHTMLを返します。


ユーザーがページにアクセスする度にサーバーでHTMLを生成するので、レスポンスに時間がかかります。

常に最新の情報でHTMLを生成するので、動的ページに向いたレンダリング方法です。
動的ページとはアクセスするユーザーや端末によって異なる内容が表示されるページです。例)ECショップのマイページ、TwitterやInstagramなどのSNS

検証ページで動作を確認

SSRの検証ページを確認すると、リロードするたびにタイマーの秒数が進んでいるのを確認できます。
これはページにアクセスしたら、その都度HTMLを生成している事になります。

秒数に注目!

getServerSideProps()

getServerSideProps関数を記述したページのレンダリング手法は、SSRになります。
ユーザーがページにアクセスするたびに、サーバー側で実行されるメソッドとなります。

以下は、APIからデータを取得する最低限のコードです。最終的にreturnで返却するオブジェクトのpropsプロパティに、APIで取得したデータを渡します。

import { GetServerSideProps } from "next";

export const getServerSideProps: GetServerSideProps = async () => {
  console.log("getServerSideProps(SSR) invoked");

  // APIデータ取得
  const apiURL = "https://jsonplaceholder.typicode.com/users";
  const res = await fetch(new URL(apiURL));
  const users = await res.json();

 // propsを返却する
  return {
    props: { users }, // 取得したAPIデータをpropsに格納
  };
};

getServerSideProps関数で取得したデータは、Next.jsのページにpropsとして渡されます。
表示させるには、次のようにpropsを展開して使用します。

最低限必要なコードを抜き出しました。

/src/pages/ssr.tsx

import { GetServerSideProps, NextPage } from "next";
import { UserType } from '../../types';

type SsrProps = {
  users:UserType[];
};

 // getServerSidePropsからpropsが渡ってくる
export const SSR: NextPage<SsrProps> = ({ users }) => {
  console.log("SSR page display");
  return (
    <>
      <main>
        <h1>SSRのページ</h1>
        <p>APIからデータを取得</p>
        <table>
          <tbody>
            {users.map((user) => {
              return (
                <tr key={user.id}>
                  <td>{user.id}</td>
                  <td>{user.username}</td>
                  <td>{user.email}</td>
                </tr>
              );
            })}
          </tbody>
        </table>
      </main>
    </>
  );
};

export const getServerSideProps: GetServerSideProps = async () => {
  console.log("getServerSideProps(SSR) invoked");

  // APIデータ取得
  const apiURL = "https://jsonplaceholder.typicode.com/users";
  const res = await fetch(new URL(apiURL));
  const users = await res.json();
  return {
    props: { users },
  };
};

export default SSR;

参考ページ(公式)

Functions: getServerSideProps | Next.js
API reference for `getServerSideProps`. Learn how to fetch data on each request with Next.js.

SSG(スタティックサイトジェネレーション)

SSG(Static Site Generation)は、アプリをビルドする際に、サーバー側で事前にHTMLを生成します。HTMLを生成する際、PropsやAPIから取得したデータをHTMLに埋め込みます。

先ほどのSSRでは、クライアントのリクエストごとにHTMLを生成しましたが、SSGではビルドの際に1回だけHTMLを生成します。

以下の図だと❶〜❸はビルド時に実行されます。

クライアントからのリクエストには、ビルド時に生成されたHTMLを返すため、レスポンスが高速です。改めてビルドしないと変更箇所が反映されないので、動的なページには使用できません。

ビルドしないと変更が反映されないので、更新頻度の多くない静的ページ向けのレンダリング方法です。例)ブログや静的なホームページなど

検証ページで動作を確認

SSGの検証ページを確認すると、何度リロードしてもタイマーの秒数が進みません。
デプロイした日時「2023年05月06日 10時09分24秒」以降HTMLが生成されていない事になります。

getStaticProps()

getStaticProps関数を記述したページのレンダリング手法は、SSGになります。
アプリをビルドする時に、サーバー側で実行される関数になります。

以下は、APIからデータを取得する最低限のコードです。最終的にreturnで返却するオブジェクトのpropsプロパティに、APIで取得したデータを渡します。

import { GetStaticProps } from 'next';

export const getStaticProps: GetStaticProps = async () => {
  console.log("getStaticProps(SSG) invoked");

  // APIデータ取得
  const apiURL = "https://jsonplaceholder.typicode.com/users";
  const res = await fetch(new URL(apiURL));
  const users = await res.json();
  return {
    props: { users },
  };
};

getStaticProps関数で取得したデータは、Next.jsのページにpropsとして渡されます。
表示させるには、次のようにpropsを展開して使用します。

最低限必要なコードを抜き出しました。

/src/pages/ssg.tsx

import { GetStaticProps, NextPage } from 'next';
import { UserType } from '../../types';

type SsgProps = {
  users: UserType[];
};

// getStaticPropsからpropsが渡ってくる
export const SSG: NextPage<SsgProps> = ({ users }) => {
  console.log('SSG page display');
  return (
    <>
      <main>
        <h1>SSGのページ</h1>
        <p>APIからデータを取得</p>
        <table>
          <tbody>
            {users.map((user) => {
              return (
                <tr key={user.id}>
                  <td>{user.id}</td>
                  <td>{user.username}</td>
                  <td>{user.email}</td>
                </tr>
              );
            })}
          </tbody>
        </table>
      </main>
    </>
  );
};

export const getStaticProps: GetStaticProps = async () => {
  console.log("getStaticProps(SSG) invoked");

  // APIデータ取得
  const apiURL = "https://jsonplaceholder.typicode.com/users";
  const res = await fetch(new URL(apiURL));
  const users = await res.json();
  return {
    props: { users },
  };
};

export default SSG;

SSRの時とコードが似ていますが、関数getServerSideProps(SSR)が、getStaticProps(SSG)に変わっただけで、内容は同じです。

ページ(公式)

Functions: getStaticProps | Next.js
API reference for `getStaticProps`. Learn how to use `getStaticProps` to generate static pages with Next.js.

ISR(インクリメンタル静的再生成)

ISR(Incremental Static Regeneration)は基本的にはSSGと同様、ビルド時にHTMLを生成します。SSGとの違いは、ページのアクセスやリンクをトリガーとして、最新のデータでHTMLを再生成することです。

トリガーが発生したら、最新データでHTMLを再生成しますが、その時は、更新前の古いページをクライアントに返します。最新ページを受け取れるのは、次のリクエストの時になります。

以下の図だと、まずビルド時に❶〜❸が実行され、トリガーが発生したらまた❶〜❸が実行されることになります。

トリガー以外にビルド時にもHTMLを生成します。ビルド後の初回アクセスではビルド時に生成したHTMLをクライアントに返却します。
❸で生成したHTMLは次のリクエストでクライアントに返却されます。

トリガーが発生しないと、HTMLが最新の情報で再生成されないので、ある程度アクセスがあるページ向けのレンダリング方法です。

検証ページで動作を確認

ISRの検証ページを確認すると、最初のリロードでタイマーの秒数は変わりませんが、サーバーでは最新の日時でHTMLが生成されています。このHTMLは次にリロードした時に表示されます。

このサンプルでは、HTMLが再生成されるのは5秒間に1度だけです。この間隔はrevalidateの設定で変更することができます。トリガーが連続で発生しても、revalidateで設定した秒数の間はHTMLは再生成されません

getStaticProps() ※revalidate設定あり

getStaticProps関数の戻り値のオブジェクトに、revalidateプロパティを設定したページのレンダリング手法は、ISRになります。
アプリをビルドした時、またはトリガーが発生した時に、サーバー側で実行される関数になります。

以下は、APIからデータを取得する最低限のコードです。最終的にreturnで返却するオブジェクトのpropsプロパティにAPIで取得したデータを渡します。

import { GetStaticProps } from 'next';

export const getStaticProps: GetStaticProps = async () => {
  console.log('getStaticProps(ISR) invoked');

  // APIデータ取得
  const apiURL = "https://jsonplaceholder.typicode.com/users";
  const res = await fetch(new URL(apiURL));
  const users = await res.json();

  return {
    props: { users },
    revalidate: 5, // 5秒に1回だけ再生成
  };
};

revalidateプロパティには、HTMLが再生成される間隔を秒数で設定します。それ以外はSSGのコードと全く同じです。

ページ(公式)

Functions: getStaticProps | Next.js
API reference for `getStaticProps`. Learn how to use `getStaticProps` to generate static pages with Next.js.

CSR(クライアントサイドレンダリング)

CSR(Client Side Rendering)は、HTMLの生成やAPIからのデータ取得などを、クライアント側で処理します。ちなみにですが、React単体はCSRに分類されます。

Next.jsでは主にuseEffect()にクライアント側で実行する処理を記述します。Next.jsは通常、事前レンダリングになるので、CSRは単体ではなく、SSGやISRと組み合わせて使用することになります。

SSGで静的コンポーネント、CSRで動的コンポーネントを作成して組み合わせることで、最新のデータであるにも関わらず、ページの読み込みも早く、SEO対策もできるページが作成できます。

検証ページで動作を確認

CSRの検証ページを確認すると、リロードした際、外部APIを取得するまでタイムラグがあるためユーザー情報がちらつきます。

※APIデータ(ユーザー情報)以外は、事前レンダリングになるので、先に表示されています。

CSF(クライアントサイドデータフェッチ)

クライアント側でHTMLの生成やAPIからデータを取得する事をまとめてCSRと呼びますが、クライアント側からAPIデータを取得する事をCSF(Client side Fetching)と呼ぶそうです。似たような言葉があってややこしいです。。

以下は、クライアント側からAPIデータを取得する最低限のコードです。
クライアント側でAPIデータを取得するコードはuseEffect()内に記述し、APIから取得したデータはuseState()で管理します。この辺の考え方はReactと同じなので割愛します。

/src/pages/csr.tsx

import {  NextPage } from 'next';
import { useState, useEffect } from 'react';
import { UserType } from '../../types';

export const CSR: NextPage = () => {
  console.log('CSR page display');

  const [users, setUsers] = useState<UserType[] | null>(null);
  const [timeStamp, setTimeStamp] = useState(0);

// 読み込み時1回だけ実行
  useEffect(() => {
    // APIデータ取得
    const fechUserData = async () => {
      const apiURL = "https://jsonplaceholder.typicode.com/users";
      const res = await fetch(new URL(apiURL));
      const users = await res.json();
      // データセット
      setUsers(users);
    };
    fechUserData();
  }, []);

  return (
    <>
      <main>
        <h1>CSRのページ</h1>
        <p>APIからデータを取得</p>
        {users && (
          <table>
            <tbody>
              {users.map((user) => {
                return (
                  <tr key={user.id}>
                    <td>{user.id}</td>
                    <td>{user.username}</td>
                    <td>{user.email}</td>
                  </tr>
                );
              })}
            </tbody>
        </table>
        )} 
      </main>
    </>
  );
};

export default CSR;

クライアント側で効率よくAPIデータを取得する方法として、React HooksのSWRを使うことが推奨されています。今度別の記事でまとめたいと思います!

まとめ

Next.jsのレンダリング方法を改めてまとめましたが、結構あやふやに覚えていたことが多かったです。レダリング手法を意識しなくてもそれなりのページは作成できますが、外部APIとの連携では必要になる知識だと思いました。