はじめに
Next.jsのSSG、SSR、CSR、ISRについて、初心者のときに最初に知りたかったポイントをまとめる😊
この記事で伝えたいこと
- 【前提】4種類のレンダリングSSG、SSR、CSR、ISRとは?
- 【疑問】どのページでどのレンダリングになるか、どうやって決まるの?
- 【具体例】SSG、SSR、CSR、ISRを図とサンプルコードでスッキリ理解。
- 【番外編】強制的にSSR、ISRにする方法。
【前提】SSG、SSR、ISR、CSRって何?
簡単に4種類のレンダリングについてまとめる。
SSG
- あらかじめHTMLを作っておく方式。
-
ビルド時にHTMLを作っておくので表示が早い⭕️
(具体的にはコマンドnpm run buildでビルドした時点でHTMLが生成される。)
-
SSGができるページはSSGがおすすめ。
(SSGができるページとは、ビルド時でもHTMLに必要なデータが用意できるページ。)
(例:会社概要ページなど)
SSR
- リクエストがきたときにHTMLを作る方式。
-
毎回HTMLを作るので表示が遅い❌
(サーバーが毎回頑張ってHTMLを作ってくれる🥲サーバーの負担やばそう🙏)
-
SSGができないページだけSSRを使う。
(SSGができないページとは、ビルド時ではHTMLに必要なデータが用意できないページ。つまりリクエスト時でないと用意できないデータがある。)
(例:現在の天気を表示するページなど)
ISR
- SSGの進化版。
-
あらかじめHTMLを作っておく方式。ただし一定の間隔で自動的に再ビルドする。
(定期的にコマンドnpm run buildが実行されてHTMLが生成されるイメージ)
-
ビルド時にHTMLを作っておくので表示が早い⭕️
(SSGと同じ)
-
SSGとSSRの中間のページでISRを使う。
(SSGとSSRの中間のページとは、「SSRのように常に最新でなくてもいいけど、SSGのように常に古い状態なのも嫌!1時間に一回くらいは新しいデータに更新してほしい🥹」のようなページ。)
(例:1時間ごとに天気データを更新するページなど)
CSR
- クライアント側でHTMLを作る方式。
-
毎回クライアント側でデータ取得やHTMLの構築をするので表示が遅い❌
(ブラウザが毎回頑張ってHTMLを作ってくれる🥲ブラウザの負担やばそう🙏)
-
動的にデータを更新したいページだけCSRを使う。
(動的にデータを更新したいページとは、ユーザー操作などに応じてクライアント側で随時データを更新したいページ。)
(例:Todoアプリのタスク一覧ページなど)
使い分けまとめ
- SSG:ビルド時にデータを取得可能なページ
- SSR:常に最新のデータが必要なページ
- ISR:最新のデータでなくてもいいが、適度にデータを更新したいページ
- CSR:クライアント側で動的にデータを更新したいページ
【疑問】Next.jsにおけるSSG、SSR、ISR、CSRの決まり方
背景
Next.jsはページごとにSSG、SSR、ISR、CSRなどを変えられるのが便利って聞いた😊
疑問が生まれた
でもページごとにSSG、SSR、ISR、CSRのどれになるか、どうやって決まるの?😫
→自分で「このページはSSGだよ!」みたいな宣言をするの?💭
→それとも「こういうページは自動でSSGになる!」みたいな条件があるの?💭
【答え】SSG、SSR、ISRになる条件が決まっている
データ取得処理がないページ:SSG
✅データ取得がないページ(fetchを使わないページ)
→常にSSG
※ただし強制的にSSRやISRにする方法もあるので後ほど紹介する。
fetchによるデータ取得処理があるページ:SSG、SSR、ISR
✅データ取得があるページ(fetchを使うページ)
→SSG、SSR、ISRが選べる。
具体的にはfetchの第二引数で決まる。
// SSG
fetch(URL); // 初期値 { cache: 'force-cache' } なので省略可
fetch(URL, { cache: 'force-cache' });
// SSR
fetch(URL, { cache: 'no-store' });
// ISR
fetch(URL, { next: { revalidate: 10 } });
Pages Routerの場合
fetchの第二引数ではなく、getStaticProps関数 か getServerSideProps関数 でSSGとSSRとISRが切り替わる。
- getStaticProps関数でデータ取得するとSSGまたはISRになる。
- getServerSideProps関数でデータ取得するとSSRになる。
fetch以外のデータ取得処理があるページ:SSG
✅データ取得があるページ(DBからデータ取得などfetchを使わないページ)
→常にSSG
※ただし強制的にSSRやISRにする方法もあるので後ほど紹介する。
CSRは単体で使えない
✅CSRは他のレンダリング手法と組み合わせて使う。
【具体例】データ取得がないページ
✅データ取得がないページは自動でSSGになる。
SSG
✅何もしなくても自動でSSGになる。
イメージ図
-
HTML JS CSSを生成(SSG)
→ユーザーがアクセスする前(ビルド時)にサーバー上でHTML等を用意しておく。
- HTTPリクエスト
-
HTTPレスポンス
→ユーザーがアクセスすると用意しておいたHTML等を返す。
サンプルコード
✅データ取得がないので自動的にSSGになるサンプル。
例:「Hello, World!」を表示するページ
app/page.js
// 「①HTML JS CSSを生成」に相当
const Page = async () => {
return <h1>Hello, World!</h1>
};
export default Page;
【具体例】データ取得があるページ(fetch)
✅fetchでデータ取得するページはSSG、SSR、ISRが選択できる。
SSG
✅fetch関数の第二引数に{ cache: 'force-cache' }
を指定するとSSGになる。
イメージ図
- データ取得(SSG)
-
データ取得(SSG)
→ユーザーがアクセスする前(ビルド時)にfetch関数でデータを取得する。
-
HTML JS CSSを生成(SSG)
→取得したデータを使ってHTMLを生成する。
- HTTPリクエスト
-
HTTPレスポンス
→ユーザーがアクセスすると用意しておいたHTML等を返す。
サンプルコード
✅fetch関数の第二引数に{ cache: 'force-cache' }
を指定し、SSGを有効にするサンプル。
例:APIで取得したデータ(IDとタイトル)を表示するページ
app/page.js
// 「①②データを取得」に相当
async function getData() {
const res = await fetch('https://jsonplaceholder.typicode.com/todos/1', { cache: 'force-cache' }) // 第二引数は省略してもOK
return res.json()
}
// 「③HTML JS CSSを生成」に相当
const Page = async () => {
const data = await getData()
return (
<div>
<h1>SSGで取得したデータ:</h1>
{data && (
<ul>
<li>id: {data.id}</li>
<li>title: {data.title}</li>
</ul>
)}
</div>
)
};
export default Page;
Pages Routerの場合
App Routerのfetch(URL, { cache: 'force-cache' });
は、Pages RouterのgetStaticProps
でのfetchと同じ。
// 「①②データを取得」に相当
export async function getStaticProps() {
const res = await fetch('https://jsonplaceholder.typicode.com/todos/1')
const data = await res.json()
return { props: { data } }
}
// 「③HTML JS CSSを生成」に相当
const Page = async ({ data }) => {
return (
<div>
<h1>SSGで取得したデータ:</h1>
{data && (
<ul>
<li>id: {data.id}</li>
<li>title: {data.title}</li>
</ul>
)}
</div>
};
export default Page;
SSR
✅fetch関数の第二引数に{ cache: 'no-store' }
を指定するとSSRになる。
イメージ図
-
HTTPリクエスト
→ユーザーからアクセスがくる。
- データ取得(SSR)
-
データ取得(SSR)
→ユーザーがアクセスしてからfetch関数でデータを取得する。
→アクセスがあるたび最新のデータ取得。
-
HTML JS CSSを生成(SSR)
→取得したデータを使ってHTMLを生成する。
-
HTTPレスポンス
→生成したHTML等を返す。
サンプルコード
✅fetch関数の第二引数に{ cache: 'no-store' }
を指定し、SSRを有効にするサンプル。
例:APIで取得したデータ(IDとタイトル)を表示するページ
app/page.js
// 「②③データを取得」に相当
async function getData() {
const res = await fetch('https://jsonplaceholder.typicode.com/todos/1', { cache: 'no-store' });
return res.json()
}
// 「④HTML JS CSSを生成」に相当
const Page = async () => {
const data = await getData()
return (
<div>
<h1>SSRで取得したデータ:</h1>
{data && (
<ul>
<li>id: {data.id}</li>
<li>title: {data.title}</li>
</ul>
)}
</div>
);
};
export default Page;
Pages Routerの場合
App Routerのfetch(URL, { cache: 'no-store' });
は、Pages RouterのgetServerSideProps
でのfetchと同じ。
// 「②③データを取得」に相当
export async function getServerSideProps() {
const res = await fetch('https://jsonplaceholder.typicode.com/todos/1')
const data = await res.json()
return { props: { data } }
}
// 「④HTML JS CSSを生成」に相当
const Page = async ({ data }) => {
return (
<div>
<h1>SSRで取得したデータ:</h1>
{data && (
<ul>
<li>id: {data.id}</li>
<li>title: {data.title}</li>
</ul>
)}
</div>
);
};
export default Page;
ISR
✅fetch関数の第二引数に{ next: { revalidate: 10 } }
を指定するとISRになる。
(10秒間隔でキャッシュを破棄し、最新のデータを取得する)
イメージ図
- データ取得(ISR)
-
データ取得(ISR)
→ユーザーがアクセスする前(ビルド時)にfetch関数でデータを取得する。
→古いキャッシュは破棄して、最新のデータを取得。
-
HTML JS CSSを生成(ISR)
→取得したデータを使ってHTMLを生成する。
上記の①〜③を10秒間隔で繰り返すためほぼ最新のデータが維持できる。
(SSGだとずっと古いデータのまま)
- HTTPリクエスト
-
HTTPレスポンス
→ユーザーがアクセスすると用意しておいたHTML等を返す。
サンプルコード
✅fetch関数の第二引数に{ next: { revalidate: 10 } }
を指定し、10秒間隔で最新のデータを取得するサンプル。
例:APIで取得したデータ(IDとタイトル)を表示するページ
app/page.js
// 「①②データを取得」に相当
async function getData() {
const res = await fetch("https://jsonplaceholder.typicode.com/todos/1", {next: { revalidate: 10 }});
return res.json();
}
// 「③HTML JS CSSを生成」に相当
const Page = async () => {
const data = await getData();
return (
<div>
<h1>ISRで取得したデータ:</h1>
{data && (
<ul>
<li>id: {data.id}</li>
<li>title: {data.title}</li>
</ul>
)}
</div>
);
};
export default Page;
Pages Routerの場合
App Routerのfetch(URL, { next: { revalidate: 10 } });
は、Pages Routerのrevalidate: 10
と同じ。
// 「①②データを取得」に相当
export async function getStaticProps() {
const res = await fetch('https://jsonplaceholder.typicode.com/todos/1')
const data = await res.json()
return {
props: { data },
revalidate: 10
}
}
// 「③HTML JS CSSを生成」に相当
const Page = async ({ data }) => {
return (
<div>
<h1>ISRで取得したデータ:</h1>
{data && (
<ul>
<li>id: {data.id}</li>
<li>title: {data.title}</li>
</ul>
)}
</div>
);
};
export default Page;
【具体例】データ取得があるページ(fetch以外)
✅fetch以外でデータ取得するページは自動でSSGになる。
SSG
✅何もしなくても自動でSSGになる。
イメージ図
ここではDBから直接データを取得するパターンを考える。
- データ取得(SSG)
-
データ取得(SSG)
→ユーザーがアクセスする前(ビルド時)にDBからデータを取得する。
-
HTML JS CSSを生成(SSG)
→取得したデータを使ってHTMLを生成する。
- HTTPリクエスト
-
HTTPレスポンス
→ユーザーがアクセスすると用意しておいたHTML等を返す。
サンプルコード
✅DBからPostsテーブルを取得するサンプル。
(fetchではなく、DBから直接データを取得するため問答無用でSSGになる。)
例:DBから取得したデータ(IDとタイトル)を表示するページ
app/page.js
import prisma from "@/app/lib/prisma"; // PrismaでDB操作をするためのコードを別途定義。今回は省略。
const Page = async () => {
// 「①②データを取得」に相当
const posts = await prisma.posts.findMany();
console.log(posts);
// 「③HTML JS CSSを生成」に相当
return (
<div>
<h1>SSGで取得したデータ:</h1>
{posts && (
<ul>
<li>id: {posts[0].id}</li>
<li>title: {posts[0].title}</li>
</ul>
)}
</div>
)
};
export default Page;
【補足】fetchによるデータ取得とそれ以外のデータ取得の違い
✅fetchによるデータ取得は前述のとおり第二引数でレンダリング方法の選択ができる。
✅またそれに加えてRequest Memoizationという機能が自動的に使われる。
Request Memoizationとは
- レンダリングするときに、もし同じリクエストがあったら1つにまとめてくれる機能。
具体的には?
- レンダリング中に何回同じ内容をfetchしても1回のリクエストにまとめてくれる。
何が嬉しい?
-
Request Memoizationがない場合
- 何も考えずにあらゆる場所で同じ内容をfetchするとパフォーマンスに悪影響がある💦
- 悪影響を出さないために、親でfetchしたデータを子にバケツリレーして、同じfetchは1回にまとめる必要があり面倒くさい💦
-
Request Memoizationがある場合
- 何も考えずにあらゆる場所で同じ内容をfetchしてもパフォーマンスに悪影響がない✨
- fetchしたデータのバケツリレーが不要。好きなところで好きなだけ同じfetchをしてOK✨
それ以外のデータ取得:このfetchをまとめてくれる機能が適用されない💦
【具体例】CSR
CSRは単体で使わない
他のレンダリングと異なる点
-
ここまで出てきたSSG、SSR、ISRはすべてサーバー側の話。
SSRの図を見ると分かりやすい😊
HTML等をサーバー側で生成している。(SSG、ISRも同様)
-
一方CSRはクライアント側の話。
クライアント側でJavaScriptを実行してHTMLを完成させるのがCSR。
図にするとこんな感じ。
→CSRをするための「HTML」と「JavaScript」が必要!
【疑問】CSRをするための「HTML」と「JavaScript」はどのように用意するの?🤔
答え:サーバー側で用意する(SSG等を使う)
- Next.jsではCSR単体で動作することはない。
- SSG等と組み合わせて使う。
-
SSG等とCSRは分けて考える。
- SSG等は「サーバー側の動作」
- CSRは「クライアント側の動作」
次は具体的にSSGやSSRと組み合わせてCSRをする方法を説明する。
SSG + CSR
✅useEffectを使うと、クライアント側の処理を書くことができる。
イメージ図
-
HTML JS CSSを生成(SSG)
→ユーザーがアクセスする前(ビルド時)にサーバー上でHTML等を用意しておく。
→HTMLは未完成の状態。
- HTTPリクエスト
-
HTTPレスポンス
→ユーザーがアクセスすると用意しておいたHTML等を返す。
→この中に「データを取得するJavaScript」の処理が混ざっている。
- データ取得(CSR)
-
データ取得(CSR)
→クライアント側で「データを取得するJavaScript」が実行される。
-
取得したデータを表示(CSR)
→クライアント側で取得したデータを使ってHTMLを完成させる。
サンプルコード
✅useEffectを使いCSRを実装する(クライアント側でデータを取得する)サンプル。
例:クライアント側でAPIを叩くページ
app/page.js
"use client";
import { useState, useEffect } from "react";
const Page = () => {
const [data, setData] = useState(null);
// ✅クライアント側のデータ取得
// 「④⑤データ取得」に相当
useEffect(() => {
getData();
}, []);
async function getData() {
const res = await fetch("https://jsonplaceholder.typicode.com/todos/1");
const resJson = await res.json();
setData(resJson);
}
// 「①HTML JS CSSを生成」「⑥取得したデータを表示」に相当
return (
<div>
<h1>CSRで取得したデータ:</h1>
{data && (
<ul>
<li>id: {data.id}</li>
<li>title: {data.title}</li>
</ul>
)}
</div>
);
};
export default Page;
SSR + CSR
✅useEffectを使うと、クライアント側の処理を書くことができる。
イメージ図
-
HTTPリクエスト
→ユーザーからアクセスがくる。
- データ取得(SSR)
-
データ取得(SSR)
→ユーザーがアクセスしてからfetch関数でデータを取得する。
-
HTML JS CSSを生成(SSR)
→取得したデータを使ってHTMLを生成する。
→HTMLは未完成の状態。
-
HTTPレスポンス
→生成したHTML等を返す。
→この中に「データを取得するJavaScript」の処理が混ざっている。
- データ取得(CSR)
-
データ取得(CSR)
→クライアント側で「データを取得するJavaScript」が実行される。
-
取得したデータを表示(CSR)
→クライアント側で取得したデータを使ってHTMLを完成させる。
サンプルコード
✅useEffectを使いCSRを実装する(クライアント側でデータを取得する)サンプル。
例:サーバー側とクライアント側でAPIを叩くページ
app/page.js
"use client";
import { useState, useEffect } from "react";
const Page = () => {
// ✅サーバー側のデータ取得
// 「②③データを取得」に相当
const [dataSSR, setDataSSR] = useState(null);
getDataSSR();
async function getDataSSR() {
// SSR
const res = await fetch("https://jsonplaceholder.typicode.com/todos/2", {cache: "no-store"});
const resJson = await res.json();
setDataSSR(resJson);
}
// ✅クライアント側のデータ取得
// 「④⑤データ取得」に相当
const [dataCSR, setDataCSR] = useState(null);
useEffect(() => {
getData();
}, []);
async function getData() {
// CSR
const res = await fetch("https://jsonplaceholder.typicode.com/todos/1");
const resJson = await res.json();
setDataCSR(resJson);
}
// 「①HTML JS CSSを生成」「⑥取得したデータを表示」に相当
return (
<div>
<h1>SSRで取得したデータ:</h1>
{dataSSR && (
<ul>
<li>id: {dataSSR.id}</li>
<li>title: {dataSSR.title}</li>
</ul>
)}
<h1>CSRで取得したデータ:</h1>
{dataCSR && (
<ul>
<li>id: {dataCSR.id}</li>
<li>title: {dataCSR.title}</li>
</ul>
)}
</div>
);
};
export default Page;
※上記のコードでは1つのコンポーネントでSSRとCSRをしているが、実際の開発では別のコンポーネントに分解する方がよさそう。
【番外編】強制的にSSR、ISRにする方法
【疑問】fetchを使わない場合はSSRやISRができない?
ここまでの説明ではfetchを使った場合のみSSRやISRを選択できた。
じゃあfetchを使わないとSSRやISRにできないの?💦
【結論】できる!
「データ取得がないページ」や「データ取得があるページ(fetch以外)」でも強制的にSSRやISRにする方法がある✨
【前提】キャッシュとの関係性
実はSSG、SSR、ISRはキャッシュが密接に関係している。
キャッシュの寿命によってSSG、SSR、ISRが決まる
-
SSGはビルド時にデータの取得を1度だけ行っていた。
→これはデータを永続的にキャッシュしているということ。
-
SSRは毎回データを取得していた。
→これはデータのキャッシュ一切していないということ。
-
ISRは定期的にデータの取得をしていた。
→これは定期的に再検証(*1)しているということ。
(*1)再検証とは
✅再検証(revalidate)とはキャッシュを破棄 & 最新のデータを取得しなおすこと。
→つまりISRの動作。
強制的にSSRにする
✅export const dynamic = 'force-dynamic'
を追記するとSSRになる。
(キャッシュを無効にして、リクエストのたびに最新のデータを取得する)
サンプルコード
✅データ取得がないページをSSRにするサンプル。
例:「Hello, World!」を表示するページ
app/page.js
// ✅キャッシュを無効にする
// →SSRになる
export const dynamic = 'force-dynamic'
const Page = async () => {
return <h1>Hello, World!</h1>
};
export default Page;
参考:https://nextjs.org/docs/app/building-your-application/caching#opting-out-1
強制的にISRにする
✅revalidate(再検証)するとISRになる。
再検証には大きく2つのやり方がある。
【パターン2-1】一定間隔で再検証する
✅page.js または layout.jsに export const revalidate = 10
を追記すると再検証でき、ISRになる。
(10秒間隔でキャッシュを破棄し、最新のデータを取得する)
ISRにしたい場合
// 更新間隔を10秒にする(=10秒間隔でキャッシュ破棄 & データ取得)
export const revalidate = 10;
【補足】revalidateの指定でSSRにすることもできる
SSRにしたい場合
// 更新間隔を0秒にする(=キャッシュを無効にする)
export const revalidate = 0;
主な用途
定期的に新しい内容に更新したいページで使う。
(例:1時間ごとに天気データを更新するページ)
サンプルコード
✅データ取得がないページをISRにするサンプル。
例:「Hello, World!」を表示するページ
app/page.js
// ✅更新間隔を10秒にする(=10秒間隔でキャッシュ破棄 & データ取得)
// →ISRになる
export const revalidate = 10;
const Page = async () => {
return <h1>Hello, World!</h1>
};
export default Page;
【補足】先ほどの「fetchの第二引数」でも再検証を設定していた
「データ取得があるページ(fetch)」でISRを実現する際、以下のように指定していた。
実はこれはfetch単位で再検証の時間を設定をしていた。
// ISR
fetch(URL, { next: { revalidate: 10 } });
今回設定した方法はページ単位で再検証の時間を設定しているだけ。
export const revalidate = 10;
【パターン2-2】任意のタイミングで再検証する
✅revalidatePath関数 revalidateTag関数を実行すると再検証でき、ISRになる。
(関数実行時にキャッシュを破棄し、最新のデータを取得する)
ここではrevalidatePath関数を使った方法を解説する。
ISRにしたい場合
// トップページを再検証する(関数実行時にキャッシュ破棄 & データ取得)
revalidatePath("/");
主な用途
ページを新しい内容に更新したいタイミングで実行する。
主にServer Actions、Route Handlers(API)で使う。
(例:Server Actionsでデータベースを更新した直後)
(例:APIでデータベースを更新した直後)
サンプルコード
✅トップページをISRにするサンプル。
例:Server Actionsでデータベースを更新した直後に再検証するページ
app/page.js
// フォームが送信されたとき(Server Actions)
async function submitAction(formData: FormData) {
"use server"
// データベースに保存する処理
// ...
// ✅トップページを再検証する(キャッシュ破棄 & データ取得)
// →トップページがISRになる
revalidatePath("/");
}
export default function SampleForm() {
// 適当な入力フォーム
return (
<form action={submitAction}>
<input name="hoge" type="text" defaultValue="" />
<button type="submit">送信</button>
</form>
)
}
参考:https://nextjs.org/docs/app/building-your-application/caching#on-demand-revalidation
まとめ
SSG、SSR、ISRを切り替える方法
-
「データ取得がないページ」「fetch以外でデータ取得するページ」
- SSGにしたい場合:何もしないでOK
-
SSRにしたい場合:
export const dynamic = 'force-dynamic'
を追記する -
ISRにしたい場合(一定間隔で更新):
export const revalidate = 10
を追記する - ISRにしたい場合(任意のタイミングで更新):またはrevalidatePath関数 revalidateTag関数を実行する
-
「fetchでデータ取得するページ」
-
SSGにしたい場合:
fetch(URL, { cache: 'force-cache' });
でデータ取得する -
SSRにしたい場合:
fetch(URL, { cache: 'no-store' });
でデータ取得する -
ISRにしたい場合:
fetch(URL, { next: { revalidate: 10 } });
でデータ取得する
-
SSGにしたい場合:
SSG、SSR、ISRとキャッシュの関係
SSG | SSR | ISR | |
---|---|---|---|
fetch関数の第二引数 | { cache: 'force-cache' } | { cache: 'no-store' } | { next: { revalidate: 10 } } |
キャッシュの寿命はどうなる? | 永続的なキャッシュになる | なし | 一時的なキャッシュになる |
いつ最新のデータを取得する? | ビルド時 | リクエスト時 | 再検証時 |
いつ使えばいい? | ビルド時にデータを取得可能なページ | 常に最新のデータが必要なページ | 最新のデータでなくてもいいが、適度にデータを更新したいページ |
参考サイト
図が分かりやすい
使い分けが分かりやすい
fetch以外の方法でSSRを有効にする方法
色々なレンダリングを確認
CSR