🐕

Next.js App Router キャッシュの今

2024/01/25に公開
2

先日Vercelから「Next.js App Router Caching: Explained!」というタイトルの動画が公開されていたので、その内容をまとめることでNext.jsのキャッシュの今について整理しておこうと思います。

https://youtu.be/VBlSe8tvg4U?si=HYmOwX9wuxz6S8Oj

基本

まずNext.jsでは、静的レンダリングがデフォルトです。RSCを使用していても基本的にはビルド時にページが事前レンダリングされます。これはRoute Handlersも同様です。仮にビルド後にデータを更新してもリビルドしない限り表示は古いままであり、これは静的にレンダリングされていると言えます。
ただし、developmentとproductionでは挙動が異なります。ローカルではコードに変更を加えるたびにデータが再取得・レンダリングされるので、ローカルとビルド後の挙動に違いがあることを理解しておきましょう。

リクエストするたびに最新のデータを取得し表示したい、つまりページやRoute Handlersを動的にレンダリングしたければ、cookies()headers()noStore()などの動的レンダリングを有効にする方法を採用する必要があります。

https://nextjs.org/docs/app/building-your-application/rendering/server-components#switching-to-dynamic-rendering

静的なfetch

サーバーサイドfetchもそのまま使うとキャッシュされるので、ビルド時に一度だけデータが取得されます。

ローカルではリロードするとキャッシュからデータが返され、スーパーリロードするとキャッシュが破棄されます。
Next.jsではその挙動がわかるようなconfigを提供してくれています。以下を設定するとデータ取得時にキャッシュがヒットしたかどうかがターミナルに出力されます。つまり、リロードするとcache: HIT、スーパーリロードするとcache: SKIPと表示されるということです。これはfetchのみ対応しているので、fetchを使用している場合は使ってみるとキャッシュの挙動がわかりやすくなると思います。

next.config.js
module.exports = {
  logging: {
    fetches: {
      fullUrl: true,
    },
  },
}

https://nextjs.org/docs/app/api-reference/next-config-js/logging

動的なfetch

これは「基本」に書いた通りで、cookies()headers()noStore()など動的レンダリングを有効にする方法を採用することで、リクエストのたびに最新のデータを取得するサーバーサイドfetchを実装することができます。

unstable_cache

静的なDBリクエスト

Next.jsアプリケーションから直接DBにリクエストする、つまりAPIを介さないリクエストにおいては、fetchを使用することができません。

その場合、Next.jsが提供するunstable_cacheを使用することで、キャッシュやその再検証を実装することになります。第三引数にはオプションを指定可能で、tagsrevalidateを指定することで、サーバーサイドfetchで実現できた任意のタイミングでの再検証や時間による再検証(ISR)も実現できます。

const useGetCachedUser = (id: string) => unstable_cache(
  async () => getUser(id),
  [`user-${id}`],
  {
    tags: [`user-${id}`],
  }
);

const getCachedUser = useGetCachedUser('123');
const user = await getCachedUser();

https://nextjs.org/docs/app/api-reference/functions/unstable_cache

unstable_cacheはNext.js v14から追加された最新の機能なので、今後APIの構造が多少調整される可能性は高いとのことです。
Next.jsはApp Router導入以降、fetchを拡張する方針をとってきましたが、ユーザーやコミュニティのフィードバックを受けてその方針からの脱却を考えているとのことなので、今後unstable_cacheの活用場面は増えていくと思われます。

動的なDBリクエスト

これは単にunstable_cacheを使わないDBリクエストをすれば良いです。

const getUser = async (id) => getUser(id);

Server Actionsによる再検証

Server Actionsでデータを更新したあとにrevalidateTag()またはrevalidatePath()を実行することで、fetchと同様にunstable_cacheのキャッシュを破棄することができます。

'use server'
 
import { revalidateTag } from 'next/cache'
 
export async function createPost() {
  try {
    // ...
  } catch (error) {
    // ...
  }
 
  revalidateTag('posts');
}

https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations#revalidating-data

Webhookによる再検証

CMSなど外部システムの何かしらの操作(例えば記事更新など)によりデータに変更が加えられた場合は、Route HandlersによりWebhookを実装するのが良いでしょう。

export async function POST() {
  // ...
  
  revalidateTag('posts');
  
  return new Response('Success!');
}

ISRによる再検証

上述の通り、unstable_cacheには第三引数のオプションでrevalidateを指定することができます。以下の例では60秒ごとに再検証が行われるISRとなります。

const getCachedUser = unstable_cache(
  async (id) => getUser(id),
  [`user-${id}`],
  {
    revalidate: 60,
  }
);

まとめ

unstable_cacheの登場によりデータ取得とキャッシュにおいても直接DBにリクエストするユースケースがサポートされることになります。これによりApp Routerを採用できるプロジェクトも増えるのではないかと思います。

unstable_cache含め、App Routerの今後に期待しましょう。

GitHubで編集を提案
frontend flat

Discussion

jay-esjay-es

unstable_cache のコード例がドキュメント通りでなく、 2, 3 個目の引数を user-${id} のように変更されているのが気になりました。

  • ドキュメントにある以下の文を「キー内でひとつめの関数の引数を使える」のように解釈した?

    The cache key also includes the arguments passed to the function.

  • もしくは外のスコープに id 変数があるコードを前提としている?

いずれにせよ読んだ人が誤解を受けそうなので、ドキュメント通り 'my-app-user' と記載しておくのが良いのではないかと思いました。

itoito

実際に使用するときはキャッシュキーにidを含めたいと思うので、カスタムフックでidを渡せる形に修正しました。ご指摘大変助かります、ありがとうございます🙇‍♂️