はじめに
エンジニアを目指す26卒大学院生です。個人開発では主にNext.jsを使用することが多いです。しかし、ファイルベースルーティングなどの開発の容易さから選んでおり、Next.jsの一番の強みと言われるサーバーサイドレンダリングを十分に活用できていないと感じています。そこで、今回はそのレンダリング方法についてまとめたいと思います。なるべくシンプルにわかりやすく書いたつもりなのでぜひ一読お願いいたします!
技術系の記事についてはしっかり調べた上で掲載しておりますが、情報に間違いがあれば、気軽にコメントしていただけると幸いです!
Next.jsのバージョンは14.2.5でApp Routerを使用しています。
レンダリング種類
レンダリングとは一言で説明すると、「HTML/CSS/JavaScriptを組み合わせて、普段見ているページの状態にすること」である。Next.jsは4種類のレンダリングをサポートしている。
1. CSR(Client Side Rendering)
サーバから最小限のデータを受け取り、クライアント側でレンダリングする方式。フレームワークなしのReact単体はCSRである。
【メリット】
- 一度ページを読み込んでしまえば、ページ遷移のたびにサーバへのリクエストが発生することがないため、画面遷移が高速
- サーバの負荷が少ない → サーバのスペックに依存しないため対応サーバが多い
【デメリット】
- 初回ロード時に全データをレンダリングするため初期表示が遅い
- パフォーマンスがクライアント側のスペックに依存する
- リクエストに対して空のHTMLとJSを返すため、SEOに対応できない
【用途】
- SEOが必要ないページ
- 動的にデータを更新したいページ
2. SSR(Server Side Rendering)
サーバ側でレンダリングを行ったものをクライアントへ返す方式。
【メリット】
- 初回ロードがCSRよりも速い
- 常に最新のデータを提供できる
- 初回ロードが早い+サーバから完全なHTMLが返ってくるためSEOに強い
- クライアント側での情報漏洩の心配がなくなる
【デメリット】
- サーバにNode.js環境が必要 → 対応サーバが限られる
- リクエスト毎にレンダリングするため、ユーザ操作によって発生する動的な画面遷移に弱い(WebSocket等)
【用途】
- データのリアルタイム性が求められる大規模なアプリケーション
3. SSG(Static Site Generation)
ビルド時にページを事前に生成しておく方式。クライアント側からのリクエストに対して生成したページを返す。
【メリット】
- すでにページを生成しているため表示速度がかなり速い
- サーバから完全なHTMLが返ってくるためSEOに強い
- サーバの負荷が少ない → サーバのスペックに依存しないため対応サーバが多い
【デメリット】
- ビルド後に動的にページの内容を変更することができないため、頻繁な更新に不向き
- リアルタイム性が低い
【用途】
- ブログやホームページなどの更新頻度が低いサービス
4. ISR(Incremental Static Regeneration)
SSGにSSRを統合したようなイメージで、指定した時間間隔で再ビルドする方式。SSGのデメリットを打ち消すことができる。
【メリット】
- SSGの高速性を維持しつつ、指定した時間間隔でページを再生成できる
- 必要なページだけを更新するため大規模サイトでも効率的に最新コンテンツを提供できる
- サーバから完全なHTMLが返ってくるためSEOに強い
【デメリット】
- 更新のタイミングによっては古い情報が表示される
【用途】
- 頻繁に更新はされるがリアルタイム性があまり重視されないサービス
簡単にまとめると以下のようになる。
種類 | どこでレンダリングするか | いつレンダリングするか |
---|---|---|
CSR | クライアント | リクエスト時 |
SSR | サーバ | リクエスト時 |
SSG | サーバ | ビルド時 |
ISR | サーバ | ビルド時 + 指定した時間ごと |
実装
App Routerでは全てのコンポーネントがデフォルトでServer Component(↔Client Component)となっている。データ取得(fetch)がないコンポーネントはデフォルトでSSGになる。データ取得を行うページではSSR、SSG、ISRかオプションで選択できる。また、複数のデータをフェッチするときはコンポーネントに分割すると自動的に非同期データフェッチになる。
以下に示すサンプルコードは、データのfetchを各レンダリング手法で行うコードである。各手法を確認するためにjson-server
でモックAPIを作成し、検証した。詳しい実装内容はこちらのGitHubリポジトリにアップロードしています。ぜひ試してみてください。
1. CSR(Client Side Rendering)
ファイルの先頭にuse client
を付けるとそのコンポーネントはClient Componentになる。しかし、Server ComponentのほうがNext.jsの恩恵を多く受けるため、CSRの使用は最小限に抑えるほうが良い。また、親コンポーネントがCSRの場合、子コンポーネントは自動的にCSRになるので注意が必要(回避方法はある)。
サンプルコード
"use client";
import React, { useEffect, useState } from "react";
const CsrPage = () => {
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchData = async () => {
const res = await fetch("http://localhost:3001/products");
const data = await res.json();
setProducts(data);
setLoading(false);
};
fetchData();
}, []);
if (loading) return <p style={{ color: "red" }}>Loading...</p>;
return (
<div>
<h1>CSR</h1>
<ul>
{products.map((product) => (
<li key={product.id}>
{product.id}: {product.title}
</li>
))}
</ul>
</div>
);
};
export default CsrPage;
【CSRでしかできないこと】
- Hooks(use state, use effect等)使用時
- イベントハンドラ(on click, on change等)使用時
- クライアント操作があるコンポーネント(フォーム、ハンバーガーメニュー等)→Server Actionsを使用するとServer Componentでも使用可能
- ブラウザAPI(window, localStorage等)
2. SSR(Server Side Rendering)
fetch関数の第二引数に{ cache: "no-store" }
を記述するとSSRとなる。
サンプルコード
const SsrPage = async () => {
const res = await fetch("http://localhost:3001/products", {
cache: "no-store",
});
const data = await res.json();
return (
<div>
<h1>SSR</h1>
<ul>
{data.map((product) => (
<li key={product.id}>
{product.id}: {product.title}
</li>
))}
</ul>
</div>
);
};
export default SsrPage;
3. SSG(Static Site Generation)
fetch関数の第二引数に{ cache: "force-cache" }
を記述するとSSRとなる。バージョン14系ではデフォルトがSSGとなっているため、引数に何も指定しない場合はSSGとなる。
サンプルコード
const SsgPage = async () => {
const res = await fetch("http://localhost:3001/products", {
cache: "force-cache",
});
const data = await res.json();
return (
<div>
<h1>SSG</h1>
<ul>
{data.map((product) => (
<li key={product.id}>
{product.id}: {product.title}
</li>
))}
</ul>
</div>
);
};
export default SsgPage;
4. ISR(Incremental Static Regeneration)
fetch関数の第二引数に{ next: { revalidate: 30 } }
を記述するとSSRとなる。数値の部分はデータの再検証を行う間隔(秒)を指定する。
サンプルコード
const IsrPage = async () => {
const res = await fetch("http://localhost:3001/products", {
next: { revalidate: 30 }, // 30秒ごとに再生成
});
const data = await res.json();
return (
<div>
<h1>ISR</h1>
<ul>
{data.map((product) => (
<li key={product.id}>
{product.id}: {product.title}
</li>
))}
</ul>
</div>
);
};
export default IsrPage;
どのレンダリングにも長所・短所があるため、開発するサービスの特性に合わせて選定する必要がある。基本的にはStatic Rendering(SSG/ISR)を使用、必要に応じてDynamic Rendering(SSR)を使用する。そして、クライアントサイドの機能が必要なときのみCSRを使う。これがベストプラクティスのようだ。
おわりに
私はNext.jsの良さをまだまだ活かしきれていないなと感じました!これまでは開発の容易さと流行っているということからNext.jsを選択していましたが、今後はもっとその強みを引き出せるように実装に取り組みたいと思いました。また、Next.jsについて調べれば調べるほど、Next.jsには多くの機能があることがわかり、その奥深さに驚きました。しかし、現段階の私のエンジニア脳では、理解できなかったため、詳細については以下の参考文献を見ていただければと思います。以下の参考文献は非常にわかりやすいです!
参考文献(2024/10/20参照)