Tags: Next.js

Next.js で revalidatePath するたびに別のページがローディング表示になる事象の原因

Next.js(App Router)で、どこかのページで revalidatePath するたびに別のページの Suspense で囲んでいる部分がローディング表示になる事象が起きたので、どうしてそうなるのか詳しく調べた。

環境

  • Next.js 14.2.4

本文

結論を言うと、fetch に cache: "no-cache" オプションを付ける(あるいはそれに準ずる状態である)とそうなる。

例として下記のような実装を考える。

import Link from "next/link";
import { Suspense } from "react";

export default function Home() {
  return (
    <div>
      <Suspense fallback={<p>Loading...</p>}>
        <Content />
      </Suspense>
      <Suspense fallback={<p>Loading...</p>}>
        <NoCacheContent />
      </Suspense>
    </div>
  );
}

async function Content() {
  const res = await fetch("http://localhost:3000/api/top", {
    next: { revalidate: 10 }
  });
  const data = await res.json();
  return <p>{data.title}</p>;
}

async function NoCacheContent() {
  const res = await fetch("http://localhost:3000/api/top", {
    cache: "no-cache"
  });
  const data = await res.json();
  return <p>{data.title}</p>;
}

<Content> コンポーネントのほうは 10 秒ごとに revalidate する。
しかし実際には client-side Router Cache があるのでそう頻繁に revalidate されることはない。
client-side Router Cache が切れるたびに revalidate するような挙動を期待している。

一方 <NoCacheContent>cache: "no-cache" オプションをつけている。
レスポンスはキャッシュされず、client-side Router Cache が切れるたびに最新のデータを取り直す。

これら二つのコンポーネントは以下のような挙動となる。

  • <Content> コンポーネント
    • ローディング UI は最初の fetch のときと、このコンポーネントが表示されているページに対して revalidatePath を実行したときに表示される。
    • client-side Router Cache が切れるたびにキャッシュデータを更新するが、画面への表示はキャッシュデータが先行する。つまり、前回 client-side Router Cache が切れた瞬間のデータを、次に client-side Router Cache が切れたときに表示する……という感じでデータの鮮度がワンテンポ遅れる。
  • <NoCacheContent> コンポーネント
    • ローディング UI は最初の fetch のときと、client-side Router Cache が切れたときと、このコンポーネントが表示されているページに対して revalidatePath を実行したときに表示される。
      • revalidatePath の現在の挙動はすべてのルートの client-side Router Cache を削除するため、このコンポーネントでは別のページに対して revalidatePath を実行したときもローディング UI が表示されてしまう。
    • client- side Router Cache が切れたときに fetch したデータを表示するため、データの鮮度がワンテンポ遅れるということはない。

client-side Router Cache は 30 秒に 1 回は切れるため、<NoCacheContent> コンポーネント側は 30 秒に 1 回以上ローディング UI を表示することになる。

実際のところ、この挙動は非常に鬱陶しい。
cache: "no-cache" オプションをつけなければならないのなら Suspense を使ってサーバーサイドで取得するのはやめたほうがいい。

以上