💬

CSR SSR SSG ISR

2023/11/30に公開

はじめに

よくあるテーマフロントエンドのレンダリング手法のテーマですが、コードベースでどのように実装すればいいのか分からなかったのでまとめてみました。

CSR

アクセスしたときに、クライアント側でデータを取得するレンダリング手法。CSR の場合はクライアント側でデータを取得して HTML を構築します。データ取得時にローディングを挟むとクライアントでデータを取得している様子を確認することができます。以下がコードになります。コードが長くなるので 型など一部省略しています。

export default function Home() {
  const [data, setData] = useState<ChildComponentProps>(null);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    async function fetchData() {
      try {
        const response = await fetch("https://umayadia-apisample.azurewebsites.net/api/persons/Shakespeare");
        const jsonData: ChildComponentProps = await response.json();
        setData(jsonData); // データセット
      } finally {
        setIsLoading(false); // ローディング状態解除
      }
    }

    fetchData();
  }, []);

  if (isLoading) {
    return <p>データを読み込んでいます...</p>;
  }

  // データが存在しない場合
  if (!data) {
    return <p>データが見つかりません。</p>;
  }

  // データが取得できた場合、以下のUIをレンダリング
  return (
    <div>
      <p>CSR</p>
      <p>Name: {data.data.name}</p>
      <p>Note: {data.data.note}</p>
      <p>Age: {data.data.age}</p>
    </div>
  );
}

下記が実際の挙動になります。

SSR

サーバー側でデータを取得してそれをクライアントに返すレンダリング手法です。

CSR の場合はクライアント側でデータを取得して HTML を構築するのに対して SSR はサーバー側でデータを取得して サーバー側で構築した HTML クライアントに返します。また、クライアント側ではなくサーバー側でデータを取得するのでローディングは走らないです。
要するに、「CSR との違いは クライアント側とサーバー側のどちらでデータを取得してHTML を構築するか」だけです。以下がコード例になります
基本的に処理をするのはクライアントよりもサーバーのほうが早いことが多いので SSR のほうがレンダリングは早くなる。しかし、サーバーを立てる必要があるので運用の手間が発生してしまうのが大きなデメリットです。

export default function Home({ data }: ApiResponse) {
  return (
    <>
      <div>
        <p>SSR</p>
        <p>Name: {data.name}</p>
        <p>Note: {data.note}</p>
        <p>Age: {data.age}</p>
        <p>Register Date: {data.registerDate}</p>
      </div>
    </>
  );
}

export const getServerSideProps: GetServerSideProps<ApiResponse> = async () => {
  const res = await fetch("https://umayadia-apisample.azurewebsites.net/api/persons/Shakespeare");
  const { success, data }: ApiResponse = await res.json();

  return {
    props: { success, data },
  };
};

CSR のときには Home にデータをフェチする処理を書いていましたが、SSR では getServerSideProps でデータをフェチする処理を記載します。ここに記述することでクライアント側ではなくサーバー側でデータを取得することができます。

処理の流れを簡単にまとめました。

  • ユーザーがHomeページにアクセスすると、Next.jsサーバーはそのリクエストを受信します。
  • データの取得:Next.jsはgetServerSideProps関数を実行します。この関数はサーバーサイドで動作し、外部APIからデータを取得するためにfetchを使用しています。
  • propsとして渡されたデータを持つHomeコンポーネントがサーバーサイドでレンダリングされます。これにより、ユーザーに表示するためのHTMLが生成されます。
  • HTMLの送信:生成されたHTMLは、ユーザーのブラウザに送信され、ページが表示されます
  • ブラウザでの表示:ブラウザは受信したHTMLを解析し、ユーザーにページを表示します。この時点で、data.name、data.noteなどのデータがページ内に表示されます。

下記が SSR の実際の挙動です。

CSR はクライアント側でデータの取得が完了するまでローディングを走らせていましたが、SSR はページにアクセスする前にサーバー側でデータを取得し HTML を構築しているのでアクセス時のローディングを走らせることなく表示できます。

SSG

SSR のサーバー側で処理をしてくれるのはいいけどページにアクセスするたびにサーバー側でデータフェチをして、HTML を構築するのはパフォーマンス的によくないです。それだったら予めデータをフェチしサーバー側で HTML を構築しておいてページにアクセスしたときにそれを返すだけにすれば結構早くなるんじゃね?その考えがもとになって生まれたのが SSG です。
SSG はビルド時にバックエンド側からデータを取得してHTML を組み立てておきページにアクセスしたときにすでに構築済みの HTML を返すというレンダリング手法です。以下がコード例になります。

export default function Home({ data }: ApiResponse) {
  return (
    <>
      <div>
        <p>SSG</p>
        <p>Name: {data.name}</p>
        <p>Note: {data.note}</p>
        <p>Age: {data.age}</p>
        <p>Register Date: {data.registerDate}</p>
      </div>
    </>
  );
}

export const getStaticProps: GetStaticProps<ApiResponse> = async () => {
  const res = await fetch("https://umayadia-apisample.azurewebsites.net/api/persons/Shakespeare");
  const { success, data }: ApiResponse = await res.json();

  return {
    props: { success, data },
  };
};

上記が実際の挙動です。裏側ですでに構築しているので SSR や CSR よりも若干表示が早いなという実感はありました。

getStaticProps の中でデータフェチの処理を書いた場合、ビルド時にそのデータの取得が行われます。SSR とそこまで書き方は変わらないですね。

これめっちゃ便利そうじゃん!って思いましたが一つ気になったことがありました。それはビルドのタイミングで HTML を構築するみたいだけど、ビルドっていつ行われるんだっけ?早速ChatGPT に聞いてみました。

  • 開発者が手動でビルドを実行したとき
  • ソースコードがリポジトリにプッシュされたとき
  • 定期的なスケジュールに基づいて
  • コンテンツ管理システム(CMS)に変更があったとき
  • プルリクエストやマージリクエストが作成されたとき

実際の挙動になります

これを見て疑問に思った方もいるのではないでしょうか。「このタイミングでしかビルドしないならすでにビルドしてデータを取得し構築された HTML に関してはデータを更新したときや追加の変更は反映されないのではないか?」と僕は思いました。
この疑問に関して以下の処理をもとに説明していきます。

以下のコードは SSG で実装したものになります。パスごとにページの表示が変わる処理になっています。記事の詳細ページやユーザーのプロフィールページなどをイメージしてもらえるとわかりやすいと思います。

import { GetStaticProps, GetStaticPaths } from 'next';

type Props = {
  postId: string;
  post: string;
};

export default function Page({ postId, post }: Props) {
  return (
    <>
      <h1>ブログ</h1>
      <h2>ID: {postId} の記事</h2>
      <article>{post}</article>
    </>
  );
}

const fetchContent = (articleId: string): string => {
  switch (articleId) {
    case 'first':
      return '初投稿です!これからよろしくお願いします。'
    case 'second':
      return 'SSG を勉強中!'
    case 'third':
      return 'Next.js は楽しい!'
    case 'fourth':
      return '勉強することが多くて大変だ〜'
    case 'fifth':
      return 'これからも頑張ります!'
    default:
      return 'no content'
  }
};

export const getStaticProps: GetStaticProps = async (context) => {
  const articleId = context.params?.id as string;
  const article = fetchContent(articleId);

  return {
    props: { articleId, article },
  };
};

export const getStaticPaths: GetStaticPaths = async () => {
  return {
    paths: [
      { params: { id: 'first' } },
      { params: { id: 'second' } },
      { params: { id: 'third' } },
    ],
    fallback: "blocking",
  };
};

fetchContent は、指定されたarticleIdに基づいて異なるコンテンツ(記事の内容)を返す役割を持っています。これは、外部のAPIやデータベースから記事を取得する実際の処理を模倣したもので、静的なウェブサイトやデモでよく使われる方法です。要するにモックみたいなものですね。

getStaticPaths 関数の中にfallbackという関数があり、false, true, blocking の値を設定することができます。

  • true
    • これを設定すると getStaticPaths に設定していないパスにアクセスすると、そのパスに初回では HTML だけを最初に返して、クライアントからデータを取得して表示する、いわゆる CSR 的な処理が行われます。2回目以降のアクセスに関しては、ページがサーバー側で生成され、SSG の動きになります。
  • "blockng"
    • これを設定すると getStaticPaths に設定していないパスにアクセスすると、このパスに初回では サーバー側でデータをフェチして HTML を構築しクライアントに表示する、いわゆる SSR 的な処理が行われます。2回目以降のアクセスに関しては、ページがサーバー側で生成され、SSG の動きになります。

つまり、trueblockingの違いは初回アクセスのときにCSR 的な動きをするのか SSR 的な動きをするのかだけですね。画面が完全な状態で常時したいなら blocking が無難だと思います。この設定は例えば、記事を新しく追加したときに getStaticPathsに存在しないパスにアクセスする際に使用します。

以下が処理の流れです。

  • ビルド時のデータ取得:getStaticPropsはビルド時に実行され、パスパラメータに基づいてfetchContent関数を使って特定の投稿の内容を取得します。
  • 静的パスの生成:getStaticPaths関数はビルド時に呼ばれ、どの静的ページが生成されるかをNext.jsに伝えます。ここで指定されたpaths(first, second, third)は、ビルド時に生成されるページのパスです。
  • fallbackの処理:fallbackがtrueに設定されているため、ビルド時に生成されていないページへのアクセスがあった場合、ユーザーのリクエストに応じてその場でページが生成されます。これにより、リストにないID(例えばfourth, fifth)のページにアクセスした場合も、サーバーはリクエストに動的に対応し、新しいHTMLページを生成して提供します。
  • ページコンポーネントのレンダリング:Pageコンポーネントは、getStaticPropsから受け取ったprops(postIdとpost)を使ってレンダリングされます。このコンポーネントは、ビルド時に生成された静的なHTMLとしてサーバーから提供されるか、またはfallbackがtrueの場合はリクエスト時に生成されます。
  • ユーザーへのページ提供:ユーザーがブラウザでブログページにアクセスすると、事前にビルドされた静的ページがすぐに表示されるか、またはfallbackによって動的に生成されたページが表示されます。これにより、ロード時間が短縮され、ユーザーエクスペリエンスが向上します。

データ追加時は動的にページを追加することができるのはわかったけど、すでに DB に有るデータを更新のときはどうなるんだ?
これは ISR のところで説明します。

ISR

SSGでデータの追加時の挙動ははわかったけど、更新のときはどうなるんだ?
これに関しては以下のコードの例に説明します。

export default function Home({ personData }: HomeProps) {
  return (
    <div>
      <p>ISR</p>
      <p>Name: {personData.data.name}</p>
      <p>Note: {personData.data.note}</p>
      <p>Age: {personData.data.age}</p>
    </div>
  );
}

export const getStaticProps: GetStaticProps<ApiResponse> = async () => {
  const res = await fetch("https://umayadia-apisample.azurewebsites.net/api/persons/Shakespeare");
  const { success, data }: ApiResponse = await res.json();

  return {
    props: { success, data },
    revalidate: 60,

  };
};

これは先程 SSG のところで説明したコードとほぼ同じです。一箇所 revalidate を追加しました。これはビルド時にページを生成してそこからどのくらいの感覚でページを再生成するのかを設定することができます。単位は秒です。この設定を行うことでデータを更新したときにページを再生成するタイミングでそのデータの更新がページに反映されます。

実際の挙動になります

最後に

いかがでしたでしょうか。いままでレンダリング手法の理解度が 50%位だったのですが、最近ようやく Next.js のレンダリング手法の仕組みを完全に理解することができました。バックエンドとフロントエンド両方とも担当していると基本的な部分を抑えるのに時間がかかる、、、

Discussion