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を生成します。
これだけだと全然わからないので詳しくみていきます!
参考ページ(公式)
レンダリング検証用サンプル
レンダリングページを種類ごとに再現したページをVercelにデプロイしました。動作はこちらで確認できます。このページで挙動を検証してみます!
コードはGitHubで確認できます。
TypeScriptなどレンダリングに直接関係ないコードはこちらで確認できます。
それぞれのページに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を参考にします。
開発モード(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を生成するので、レスポンスに時間がかかります。
検証ページで動作を確認
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;
参考ページ(公式)
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)に変わっただけで、内容は同じです。
ページ(公式)
ISR(インクリメンタル静的再生成)
ISR(Incremental Static Regeneration)は基本的にはSSGと同様、ビルド時にHTMLを生成します。SSGとの違いは、ページのアクセスやリンクをトリガーとして、最新のデータで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のコードと全く同じです。
ページ(公式)
CSR(クライアントサイドレンダリング)
CSR(Client Side Rendering)は、HTMLの生成やAPIからのデータ取得などを、クライアント側で処理します。ちなみにですが、React単体はCSRに分類されます。
Next.jsでは主にuseEffect()にクライアント側で実行する処理を記述します。Next.jsは通常、事前レンダリングになるので、CSRは単体ではなく、SSGやISRと組み合わせて使用することになります。
検証ページで動作を確認
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との連携では必要になる知識だと思いました。