nino
  • ドキュメント
  • レジストリ
  • メンバーシップ

Command Palette

Search for a command to run...

スポンサー
メニュー

Cache Component

 

7分 35秒

ライブデモ

Cache Components の考え方

  • Next.js はユーザーの待機時間を減らしたい
  • ユーザーは主にデータ取得を待つことになる
  • そこで、取得したデータをキャッシュし、再利用することですぐにデータを含んだ結果を表示させる
  • あるいは、データ取得部分を遅れて表示させる(Suspense)ことでそれ以外の部分をすぐに表示させる

つまり、ユーザーを待たせる原因となるデータ取得を

  • use cache でキャッシュして再利用
  • Suspense で部分的に遅延表示

の二択で解決するのが Cache Components です。

キャッシュコンポーネントは現在オプションなので、 next.config.mjs で有効にする必要があります。

Loading...

キャッシュコンポーネントを有効にすると、データ取得の実装をした際に Suspense or キャッシュ を強いるアラートが表示されるようになります。これによりユーザーは半強制的にいずれかのパフォーマンス改善を行うことになります。

キャッシュ

データが入った状態の画面が即座に表示されるのが理想なので、基本的にはキャッシュを使います。キャッシュはファイルやコンポーネント、関数に対し "use cache" を使います。

Loading...

キャッシュはいつ更新されるのか

キャッシュは以下のタイミングで更新されます。

  • キャッシュ生成から一定時間経過後にアクセスがあった場合
    • デフォルトでは15分
    • cacheLife で変更可能
  • revalidatePath で該当のパスを指定したとき
  • updateTag で該当のタグを指定したとき
  • revalidateTag で該当のタグを指定したとき

属人的データは use cache: private を使う

まずビルド時点でデータ取得が行われ、その結果がキャッシュされ、その後のユーザーアクセス時にキャッシュが再利用されます。

しかし、ユーザーの情報に基づいて取得するデータ(属人的データ)はビルド時点で取得できないため、通常の "use cache" では対応できません。

たとえばマイページにユーザーが投稿した記事を表示する場合、マイページにアクセスしたユーザーのIDを受け取ってはじめて記事を取得できます。

そのようなデータに対しては "use cache: private" を使い、取得した結果をユーザーのブラウザにキャッシュします。

Loading...

属人的データはユーザーIDを受け取ってはじめてデータ取得を開始できるため、絶対に待ち時間が発生します。そのため Suspense は必須になります。

上記のようにコンポーネントを分けて <Suspense> で囲むか、ページと同階層以上に loading.tsx を設置して画面全体を Suspense の対象とすることで初回の属人的データ表示を遅延させ、ビルドエラーも解決できます。

use cache: private は一定期間ブラウザにキャッシュされるので、同サイト内の別画面に遷移し、戻ってきた場合瞬時に同じデータが表示され、サーバーサイドへのリクエストは発生しません。

注意

現在 use cache: private は機能していません。

vercel/next.js

事前予測ができないが属人的ではないデータは use cache: remote を使う

記事詳細ページのように、アクセスされてはじめて取得対象の記事IDが判明するケースでは、ビルド時に事前生成することができません。

そのようなデータに対しては "use cache: remote" を使い、取得した結果をサーバーサイドにキャッシュし、ユーザー間でシェアします。

Loading...

use cache: private はユーザーの情報に基づいて取得するデータ(属人的データ)に有効ですが、ブラウザキャッシュなので他のユーザーとキャッシュをシェアできません。(セキュリティ的にもNG)

なお、記事の総量が少ない場合は generateStaticParams で事前生成することもできます。

Loading...

これにより全記事の詳細画面がビルド時点で生成&キャッシュされます。ただし記事数が多い場合ビルド時に大量のデータリクエストが発生し、ビルド時間も長くなるためおすすめできません。

Suspense

Suspense を使うとデータ取得部分を遅延させ、それ以外の部分をすぐに表示できます。

Loading...

ただし Suspense 単体で使う場合はキャッシュされないため、画面遷移のたびにデータ通信が発生します。基本的にはいずれかのキャッシュと組み合わせて使うことを推奨します。

レイアウトシフトに注意する

データ取得を待つ間、Suspense は fallback を表示します。一般的にフォールバックUIはローディングアイコンやスケルトンと呼ばれるグレーのボックスで表現します。

フォールバックUIの高さとデータ取得後の高さが異なる場合、データ取得後にがたつきが生じ、ユーザーに不快感を与えます(レイアウトシフト)。

そのため、フォールバックUIの高さとデータ取得後の高さが一致するようにします。結果の形が予測できる場合、sahdcn/ui の Skeleton がおすすめです。

Loading...

取得後のコンテンツの高さが予測できない場合、フォールバックUIに十分な高さをとったり、Suspenseの粒度を広げて後続のUIも内包するなどの工夫が必要です。

Loading...
Discord で質問する
何か気になったこと、分からないことがあれば気軽に質問してください!
DiscordDiscordで質問する
nino

Developer

XXGitHubGitHubYouTubeYouTubeZennZennDiscordDiscord

    リンク

  • ドキュメント
  • レジストリ
  • アーキテクチャ

    ツール

  • フィード
  • ステータス

    ポリシー

  • 利用規約
  • プライバシーポリシー
  • 特定商取引法に基づく表示
ドキュメント
はじめに
  • Changelog
ガイド
  • Webアプリの環境構築
  • ニュース収集&AI要約
  • Turso のテーブル移行
  • 日時の管理
  • 中規模のリストをブラウザにキャッシュし、クライアントでフィルタする
  • Search Params Dialog
  • コードの整理
  • AGENTS.md
  • Better Auth
  • プロダクト開発ポリシー
  • 推奨ツール
  • Proxy.ts
  • 多言語対応
  • SWR
  • Cache Component
  • Next.js の課題、不具合
  • アプリ開発フロー
  • プロンプトガイド
  • CSS Tips
  • Font Family
  • セルフブランディング
  • セルフオンボーディング
  • オフィスツール
  • Email
  • Breadcrumb(パンくず)
  • 並べ替え
module.exports = {
  cacheComponents: true, 
};
"use cache"; 

export default async function Page() {
  const post = await getPost();
  return <div>{post.title}</div>;
}
export default async function MyPage() {
  return <div>
    <h1>自分の記事</h1>
    <Suspense fallback={<div>Loading...</div>}>
      <Posts />
    </Suspense>
  </div>;
}

function Posts({ userId }: { userId: string }) {
  "use cache: private"; 

  const posts = await getMyPosts(userId);
  return <div>{posts.map((post) => <p key={post.id}>{post.title}</p>)}</div>;
}
export default async function PostPage({ params }: PageProps<"/posts/[id]">) {
  "use cache: remote"; 

  const postId = (await params).id;
  const post = await getPost(params.id);
  return <div>{post.title}</div>;
}
"use cache";

export const generateStaticParams = async () => {
  const posts = await getAllPosts();
  return posts.map((post) => ({ id: post.id }));
};

export default async function PostPage({ params }: PageProps<"/posts/[id]">) {
  const postId = (await params).id;
  const post = await getPost(params.id);
  return <div>{post.title}</div>;
}
export default async function Page() {
  return <div>
    <h1>記事一覧</h1>
    <Suspense fallback={<div>Loading...</div>}>
      <Posts />
    </Suspense>
  </div>;
}
import { Skeleton } from "@/components/ui/skeleton"; 

export default async function Page() {
  return <div>
    <Suspense fallback={<Skeleton className="h-10 bg-gray-200" />}>
      <UserCard />
    </Suspense>
  </div>;
}
import { Skeleton } from "@/components/ui/skeleton"; 

export default async function Page() {
  return <div>
    <Suspense fallback={<Skeleton className="h-10 bg-gray-200" />}>
      <UserCard />
      <p>後続のコンテンツ</p>
    </Suspense>
  </div>;
}