【React】4つの観点で理解するCSR・SSR・next/linkの挙動の違い

JavaScript

ReactアプリケーションにおけるCSR・SSR・next/linkの挙動の違いについて4つの観点で紹介をします。

CSRはフレームワークを利用していないReactアプリケーション、SSRはNext.jsを利用したReactアプリケーションで実装をします。

next/linkとはNext.jsのルータ機能です。Next.jsのページはPre-renderingがデフォルトですが、next/linkを利用して画面遷移をするとCSRになります。
next/linkを利用したCSRの流れの詳細解説は【Next.js】next/linkの画面遷移(CSR)から画面反映までの流れの図解解説で紹介しています。

素のReactアプリケーションのCSRと、next/linkによるCSRで挙動の違いがあるかを確認するため今回比較対象に加えています。

検証対象をまとめると以下のようになります。

今回挙動を確認する対象
  • 素のReactアプリケーションによるCSR
  • Next.jsによるSSR
  • next/linkによるCSR

reactは17.0.2、nextは11.1.2を利用しています。

検証で利用するサンプルコードについて

今回は『ローカル環境に構築したAPI経由で受け取ったToDoリストを表示するページ』で動作検証をします。CSSについては簡単のため紹介を省略します。

plain react app

import axios from "axios";
import React, { useEffect, useState } from "react";

type TodoItem = {
  id: number;
  content: string;
  completed: boolean;
};

const TodoApp: React.FC = () => {
  const [todos, setTodos] = useState([] as TodoItem[]);
  useEffect(() => {
    const getTodoRequest = async () => {
      const response = await axios.get("http://localhost:4000/todos");
      const todos = response.data;
      return todos;
    };
    getTodoRequest().then((todos) => setTodos(todos as TodoItem[]));
  }, []);

  return (
    <>
      <section>
        <h2>Todo List</h2>
        <ul>
          {todos.map(({ id, content }) => (
            <li key={id}>
              <a href={`/todos/${id}`}>{content}</a>
            </li>
          ))}
        </ul>
      </section>
    </>
  );
};
export default TodoApp;

react app with next.js

import type { NextPage } from "next";
import axios from "axios";

type TodoItem = {
  id: number;
  content: string;
  completed: boolean;
};

type PageProps = {
  todos: TodoItem[];
};

const TodoList: NextPage<PageProps> = ({ todos }) => {
  return (
    <>
      <section>
        <h2>TodoList</h2>
        <ul>
          {todos.map(({ id, content }) => (
            <li key={id}>
              <a href={`/todos/${id}`}>{content}</a>
            </li>
          ))}
        </ul>
      </section>
    </>
  );
};

export async function getServerSideProps() {
  const response = await axios.get("http://localhost:4000/todos");
  const todos = response.data;
  return {
    props: {
      todos,
    },
  };
}

export default TodoList;

検証方法について

CSRの検証はreact-scripts start、SSRの検証はnext devでReactアプリケーションを起動します。

next/linkの検証は以下のように、<Link>コンポーネントを経由することで画面にアクセスして確認します。

import type { NextPage } from "next";
import Link from "next/link";

const Home: NextPage = () => {
  return (
    <>
      <Link href={`/todos`}>
        <a>TodoList</a>
      </Link>
    </>
  );
};
export default Home;

挙動の比較結果

今回検証した項目は以下の通りです。

今回の検証項目
  • リクエスト・レスポンスの内容
  • JavaScriptを無効にしたときの挙動
  • ブラウザ上でのみ実行可能な機能の利用可否
  • レンダリング時のログ出力の場所

以下ではそれぞれの項目の結果について紹介します。

リクエスト・レスポンスの内容

Chrome DevToolsのNetworkタブを利用してリクエストとレスポンスを確認します。

CSRの場合

JavaScriptの組み込まれたHTMLが返ってきます。

HTMLのPreviewはYou need to enable JavaScript to run this app.という文字列のみで、実際の画面で表示されているUIは構築されません。

SSRの場合

実際の画面で表示されているUIを構築したHTMLが返ってきます。

このことから、サーバサイドですでにHTMLの作成がされていることがわかります。

next/linkの場合

JavaScriptとJSONが返ってきます。

Next.jsではnext/linkを利用してSSRのページに画面遷移する場合、CSRをする際に必要なPropsがgetServerSideProps()によってJSON形式でクライアントサイドに渡されます。

JavaScriptを無効にしたときの挙動

JavaScriptを無効にする方法はDisable JavaScriptをご覧になってください。

CSRの場合

You need to enable JavaScript to run this app.という文字列が表示され、UIが全く構築されません。

SSRの場合

Next.js『Pre-rendering and Data Fetching#Pre-rendering』にも記載されている通り、JavaScriptを無効にするとPre-renderingのページにCSSが反映されません。

ただし、本番モード(next build && next start)で起動した場合は事前にビルドしたCSSを利用できるため、以下のようにJavaScriptを無効にした状態でもCSSが適用されます。

next/linkの場合

JavaScriptを無効にするとJSONとJavaScriptの代わりにHTMLがレスポンスとして返されます。
CSSはレスポンスに含まれていないため、以下のようにCSSが適用されていない画面が表示されます。

ただし、本番モード(next build && next start)で起動した場合はCSSが適用された画面になります。

つまり今回のサンプルコードにおいては、JavaScriptを無効にしたときのnext/linkの挙動はSSRのそれと同じといえます。

ブラウザ上でのみ実行可能な機能の利用可否

ブラウザ上でのみ実行可能な機能の例は以下の通りです。

ブラウザ上でのみ実行可能な機能の例
  • Windowの参照
  • Cookieの参照
  • localStrageの参照
  • sessionStorageの参照

今回は以下のように警告ダイアログ(window.alert())をレンダリング時の処理に加えることで利用可否を確認します。

plain react app

const TodoApp: React.FC = () => {
  window.alert("Hello world!");
  // 略

react app with next.js

const TodoList: NextPage<PageProps> = ({ todos }) => {
  window.alert("Hello world!");
  // 略

CSRの場合

警告ダイアログが正常に表示されます。

SSRの場合

ReferenceError: window is not definedというサーバエラーになります。

SSRのページでブラウザの機能を利用する場合は副作用で実行します。
SSRにおけるブラウザ機能の利用方法の詳細解説は【Next.js】SSR/SGでブラウザ機能(Cookieなど)を活用する際の注意点で紹介しています。

next/linkの場合

警告ダイアログが正常に表示されます。

レンダリング時のログ出力の場所

以下のようにレンダリングの過程にconsole.logを仕込み、レンダリングがどの環境で行われているか確認をします。

plain react app

const TodoApp: React.FC = () => {
  console.log('rendering');
  // 略

react app with next.js

const TodoList: NextPage<PageProps> = ({ todos }) => {
  console.log('rendering');
  // 略

CSRの場合

ブラウザのコンソールに2回ログが表示されます。

2回の内訳は『初期Stateでのレンダリング』と『ToDoリストをStateにセットした際のレンダリング』です。
Stateの更新があるとReactアプリケーションは再レンダリングをするため、今回のように2回ログが表示されます。

Reactコンポーネントの再レンダリングに関する詳細解説はReactコンポーネントの再レンダリング発生条件と防止方法で紹介しています。

SSRの場合

next devを実行したターミナルと、ブラウザのコンソールに1回ずつログが表示されます。

サーバサイドでレンダリングをしてHTMLをレスポンスとして返した後、クライアントサイドではHydration時にレンダリングが行われます。

HydrationとはHTMLに付随したJavaScriptを利用してイベントリスナを登録することでインタラクティブ(操作可能)なページを生成する過程のことをいいます。

【図解】Next.jsのSSRが画面反映されるまでの具体的な流れで解説したように、Next.jsのSSRの過程ではクライアントサイドでレンダリング結果の比較・検証が行われます。

next/linkの場合

ブラウザのコンソールに1回ログが表示されます。

ターミナルにはログが表示されないため、next/linkのレンダリングはクライアントサイドだけで完結していることがわかります。

さいごに

Twitter(@nishina555)やってます。フォローしてもらえるとうれしいです!