0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[fetch] Next.js version15の破壊的変更を確認してみる

Last updated at Posted at 2025-02-02

はじめに

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

スクリーンショット 2025-02-02 10.57.24.png

SSGになりました。
表示を確認してみます。

スクリーンショット 2025-02-02 10.59.50.png

fetch apiはデフォルトでキャッシュしなくなったので、serverの内容を変えて、リロードすれば新しいデータが表示されるはずです。

app.get("/", (req: Request, res: Response) => {
  res.status(200).json([
  { id: 1, name: "tee" },
  { id: 1, name: "ren" }
  ]);
});

リロードしてみます

スクリーンショット 2025-02-02 10.59.50.png

変わりません...

原因を考えてみる

先ほど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

ssrになりました
スクリーンショット 2025-02-02 11.21.08.png
表示
スクリーンショット 2025-02-02 11.22.09.png


ここでサーバーを以下のように変更して、ページをリロードしてみます。
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" },
  ]);
});

ページをリロード
スクリーンショット 2025-02-02 11.27.54.png

予想通り更新されました。

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" }]);
});

表示
スクリーンショット 2025-02-02 11.33.02.png

ではサーバーを以下のように変更して、ページをリロードしてみます。
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" },
  ]);
});

スクリーンショット 2025-02-02 11.37.04.png

予想通り、/は更新されませんでした。

明示的に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

ssrになりました
スクリーンショット 2025-02-02 11.43.24.png

表示

スクリーンショット 2025-02-02 11.44.20.png


apiを変更してリロード

app.get("/", (req: Request, res: Response) => {
  res.status(200).json([
  { id: 1, name: "tee" },
  { id: 2, name: "ren" }
  ]);
});

スクリーンショット 2025-02-02 11.48.02.png

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は関連づけずに覚えるべきだと言うことを理解できました

スクリーンショット 2025-02-02 13.43.06.png

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?