はじめに
Next.js version 15が2024.10.21にリリースされました
このリリースではキャッシュ周りの破壊的変更が行われたので調査してみました。
前提知識
Next.jsのfetch apiがカスタムされていることへの理解
SSR, SSGの違いの理解
内容
今回注目したのはfetch apiのキャッシュ周りです。
version 14まではデフォルトでキャッシュされる仕様でした
version 15からはキャッシュされなく
なりました
参考にoptionを貼っておきます
fetch("url", { cache: "no-store" }) // キャッシュされない
fetch("url", { cache: "forcee-cache" }) // キャッシュされる
fetch("url", { next: { revalidate: 3600 } }) // 指定の時間でキャッシュを更新
公式ドキュメントを覗いてみると以下の内容を見つけました(日本語訳)
このコンポーネントはブログ投稿のリストを取得して表示します。 fetchからの応答はデフォルトではキャッシュされません。
APIを用意して実験してみる
ExpressでAPIを用意
これを叩いて実験してみます。
app.get("/", (req: Request, res: Response) => {
res.status(200).json([{ id: 1, name: "tee" }]);
});
next.js version15
type User = {
id: number;
name: string;
};
export default async function Home() {
const res = await fetch("http://localhost:3001");
const data = await res.json();
return (
<div>
{data.map((user: User) => (
<div key={user.id}>
<p>名前: {user.name}</p>
</div>
))}
</div>
);
}
$ npm run build
$ npm start
SSGになりました。
表示を確認してみます。
fetch apiはデフォルトでキャッシュしなくなったので、serverの内容を変えて、リロードすれば新しいデータが表示されるはずです。
app.get("/", (req: Request, res: Response) => {
res.status(200).json([
{ id: 1, name: "tee" },
{ id: 1, name: "ren" }
]);
});
リロードしてみます
変わりません...
原因を考えてみる
先ほどbuildした際はレンダリング方式がprerendered as static content(SSG)でした。
つまりbuild時にfetchが行われて、事前にHTMLの内容が作成された状態になります。
ユーザーがページに訪れた際はfetchのキャッシュがどうとか関係なくなります。 実行されないですからね...
公式ドキュメントを確認して、以下の内容が見つかりました。
このルートの他の場所で動的 API を使用していない場合は、静的ページに事前レンダリングされます。その後、増分静的再生成をnext build使用してデータを更新できます。
つまり、ISRにするか、SSRのような動的生成にする必要があります。
状況を整理して実験してみる
next.js version 15ではfetch apiはデフォルトでキャッシュされない
next.js version 14ではfetch apiはデフォルトでキャッシュされる
SSG(ISRなし)だとfetch apiのキャッシュ云々が関係なくなる
no-storeのfetchを1つ増やすことでSSRにしてみます。
apiを追加
app.get("/", (req: Request, res: Response) => {
res.status(200).json([{ id: 1, name: "tee" }]);
});
app.get("/posts", (req: Request, res: Response) => {
res.status(200).json([{ id: 1, name: "post1" }]);
});
no-storeのfetchを追加する
type User = {
id: number;
name: string;
};
type Post = User;
export default async function Home() {
const res = await fetch("http://localhost:3001");
const data = await res.json();
const res2 = await fetch("http://localhost:3001/posts", {
cache: "no-store",
});
const data2 = await res2.json();
return (
<>
<div>
{data.map((user: User) => (
<div key={user.id}>
<p>名前: {user.name}</p>
</div>
))}
</div>
<hr />
<div>
{data2.map((post: Post) => (
<div key={post.id}>
<p>名前: {post.name}</p>
</div>
))}
</div>
</>
);
}
$ npm run build
$ npm start
ここでサーバーを以下のように変更して、ページをリロードしてみます。
no-storeを指定している/posts
はもちろん、デフォルトでキャッシュしない/
も更新されるはずです。
app.get("/", (req: Request, res: Response) => {
res.status(200).json([
{ id: 1, name: "tee" },
{ id: 2, name: "ren" },
]);
});
app.get("/posts", (req: Request, res: Response) => {
res.status(200).json([
{ id: 1, name: "post1" },
{ id: 2, name: "post2" },
]);
});
予想通り更新されました。
Next.js version14の時と比べる
以下コマンドでversionだけ落としたものを作成してみます。
$ npx create-next-app@14.2.23
apiを元に戻します
app.get("/", (req: Request, res: Response) => {
res.status(200).json([{ id: 1, name: "tee" }]);
});
app.get("/posts", (req: Request, res: Response) => {
res.status(200).json([{ id: 1, name: "post1" }]);
});
ではサーバーを以下のように変更して、ページをリロードしてみます。
no-storeを指定している/postsは更新されるはずです。
next.js version 14ではデフォルトでキャッシュする
はずなので、/
は更新されないはずです。
app.get("/", (req: Request, res: Response) => {
res.status(200).json([
{ id: 1, name: "tee" },
{ id: 2, name: "ren" },
]);
});
app.get("/posts", (req: Request, res: Response) => {
res.status(200).json([
{ id: 1, name: "post1" },
{ id: 2, name: "post2" },
]);
});
予想通り、/
は更新されませんでした。
明示的にno-storeを指定した場合
APIは1つだけで、明示的にno-storeを指定してみます。
api
app.get("/", (req: Request, res: Response) => {
res.status(200).json([{ id: 1, name: "tee" }]);
});
next.js version15
type User = {
id: number;
name: string;
};
export default async function Home() {
const res = await fetch("http://localhost:3001", {
cache: "no-store",
});
const data = await res.json();
return (
<div>
{data.map((user: User) => (
<div key={user.id}>
<p>名前: {user.name}</p>
</div>
))}
</div>
);
}
$ npm run build
$ npm start
表示
apiを変更してリロード
app.get("/", (req: Request, res: Response) => {
res.status(200).json([
{ id: 1, name: "tee" },
{ id: 2, name: "ren" }
]);
});
build時にSSRだったのは確認していたので予想通りですね。
まとめ
Next.js version15ではfetch APIはデフォルトでキャッシュされない
他の場所で動的APIを使用していない場合SSGとして事前レンダリングされる
SSGの場合ページ訪問時にfetchが走らないので、キャッシュされているような挙動になる
no-storeを明示的に付与するとSSRとしてレンダリングされる
ちょっとパズルみたいですね。
単体でfetchを使う場合はSSGになってしまう可能性があるので気をつけるべし
今回の反省
fetchのキャッシュとレンダリング方式は別物ではありますが、fetchの方法によってある程度レンダリング方式が決まってきます。
以下のように覚えてしまうと、今回のような予期しない挙動に悩まされます。
fetch("url", { cache: "no-store" }) // SSR
fetch("url", { cache: "forcee-cache" }) // SSG
fetch("url", { next: { revalidate: 3600 } }) // ISR
そもそもこれじゃCSRを考慮できてない
正しくは以下 + 条件によってはレンダリング方式を意識する。ですかね
fetch("url") // デフォルト挙動はキャッシュされない
fetch("url", { cache: "no-store" }) // キャッシュされない
fetch("url", { cache: "forcee-cache" }) // キャッシュされる
fetch("url", { next: { revalidate: 3600 } }) // 指定の時間でキャッシュを更新
no-storeがデフォルト値として考えるのもちょっと微妙ですね
明示的に指定した場合レンダリング方式がSSRに決定されて、キャッシュしませんが、省略すると、条件によってはSSGにレンダリングされて、キャッシュされたような挙動になります。
no-storeを明示的に指定することで、SSGになるのを回避できるわけなので、
全てのfetchに明示的にno-storeをつけるのが一番事故が少ないかもしれません。
fetchはデフォルトでキャッシュしないが、気をつけないとSSGになってしまい、キャッシュされたような挙動を撮ってしまうというのは若干分かりにくような気もします。
今回fetchのキャッシュ周りの戦略が変わったように、将来的に変わる可能性もあるのかな〜という個人の感想です。
追記
公式ドキュメントのapi/function/fetchを見ると明確でした
デフォルト値はauto no cacheで、キャッシュされませんが、動的APIの使用が検出されない場合、build時に1回だけ実行されるとの説明がありまし。
やはりfetch("")のデフォルトはSSGである。SSRであるといった覚え方だとハマりそうですね。
fetchによるData cacheとSSGのようなfull route cacheは関連づけずに覚えるべきだと言うことを理解できました
参考