10
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?

【Next.js】4種類の"Cache"を理解したい Part ② Data Cache

Last updated at Posted at 2024-03-07

App Routerの登場で、より強調されたCachingについて理解を深めたいと思います。

ドキュメントに沿って、4回に分けて記事にしていきます。

  1. Request Memoization
  2. Data Cache
  3. Full Route Cache
  4. Router Cache

今回はData Cacheです。

V14までの仕様です。
V15からは、[Data Cache]とRouter Cacheがデフォルトで無効になります。
詳細はこちら

概要

ServerサイドにおけるHttp Requestの結果を永続的にCacheします。

.next/cache配下にCacheが保持されることで永続化を可能としています。

Next.Jsが従来のServerサイド(Node.Js)のFetchを拡張することで、このData Cacheを実現しています。BrowserサイドにおけるFetchは、Browserが保持するCacheとやり取りするものなので混乱しないように注意しましょう。

※永続化に関して、CDNやEdge NetworkなどのCache Serverも保存場所となるはずですが、ドキュメントのみでは詳細が分かりませんでした。

Request Memoizationとの違い

  • 保持期間
    Request MemoizationはRendaringごとに保持されるため、永続的ではありません。

  • 保存場所
    Rendaring Serverに、Cacheが保持されます。
    例えば、Data CacheがCache Server(CDNやEdge)に配置されている場合、そこへのRequest自体を削減する目的があります。今回のサンプルのように、Rendering ServerとCache Serverが同じ場合は大きな違いは出ないですね。

How it works

Rendering時は、Request Memoizationも同時に動きますが、ここではData Cacheの動きのみに注目してください。

image.png
出典 : How the Data Cache Works

  1. Rendering時に、Fetchが発生した場合、Request MemoizationとData Cacheを順にチェックし、Cacheが無ければ、Data Sourceに対してRequestがそのまま行われる
  2. Responseは、Request MemoizationとData Cacheに保存されつつ、Request元に戻される
  3. 次のFetchに { cache: 'no-stoire' }が含まれている場合は、Data Cache層でのCacheはスキップされる。ちなみに、Request MemoizationはOptionが異なるため、新規Reuqestとして認識され、Cacheを返していない

保持期間

明示的にRevalidationやCache無効化を行わない限り、永続的に保持されます。

Revalidating

2種類の方法があります。

  • Time-based Revalidation

fetch('https://...', { next: { revalidate: 3600 } })のように、時間(秒)を指定することで、時間経過後にRevalidationをおこなってくれます。

ここでポイントとなるのは、一定時間経過後に、まずData Cacheに現存するStale Cacheを返したあとに、Revalidationが行われることです。

image.png
出典 : How Time-based Revalidation Works

  • On-demand Revalidation

revalidateTag('tag-name')revalidatePath('/path-name')を実行します。

前者は、指定されたTagのData CacheをPurge(削除)し、後者は指定されたPathのRe-Renderingをに行いつつ、Data CacheのRevalidationをおこなってくれます。

image.png
出典 : How On-Demand Revalidation Works

Cache無効化

  • Fetchごとに個別に無効化する場合
fetch(`https://...`, { cache: 'no-store' })
export const dynamic = 'force-dynamic'

実際に確認

Buttonアクションで、/api配下のRouteにRequestを飛ばし、そこから外部APIサーバーにRequestを送るというサンプルを作成します。外部APIサーバーはランダム値を返すので、Cacheが利用されていない場合、Responseは全て異なることになります。

外部APIサーバー向けのRequestがData Cacheに保持されます。

実装

Project
shell
npx create-next-app@latest --typescript --tailwind
Component
src/app/data-cache/page.tsx
'use client';

export default function Home() {
  const onNormal = async () => {
    await fetch('/api/data-cache/normal');
  };

  const onTimeBased = async () => {
    await fetch('/api/data-cache/time-based-revalidation');
  };

  const onDemand = async () => {
    await fetch('/api/data-cache/on-demand-revalidation');
  };

  const onOptOut = async () => {
    await fetch('/api/data-cache/opt-out');
  };

  return (
    <div>
      <button onClick={onNormal} className="border bg-yellow-200 mt-3 mx-3 p-2">
        Normal
      </button>
      <button onClick={onTimeBased} className="border bg-blue-200 mx-3 p-2">
        Time-based revalidation
      </button>
      <button onClick={onDemand} className="border bg-green-200 mx-3 p-2">
        On-demand revalidation
      </button>
      <button onClick={onOptOut} className="border bg-red-200 mx-3 p-2">
        Opt-out
      </button>
    </div>
  );
}
Fetch Function
src/utils/app-fetch.ts
const { signal } = new AbortController();
export async function getRandomNumForDataCache(next?: NextFetchRequestConfig) {
  // Opt-out Request Memoization
  const res = await fetch('http://localhost:3005', { signal, next });
  console.log(await res.text());
}
Route Handler
src/app/api/data-cache/normal.ts
import { getRandomNumForDataCache } from '@/utils/app-fetch';

export async function GET() {
  for (let index = 1; index < 4; index++) {
    console.log(`Normal ${index}`);
    await getRandomNumForDataCache();
  }
  return new Response('', { status: 200 });
}
src/app/api/data-cache/time-based-revalidation.ts
import { getRandomNumForDataCache } from '@/utils/app-fetch';

export async function GET() {
  console.log('Time-based revalidation 1');
  await getRandomNumForDataCache({ revalidate: 5 });

  // This will hit cache
  await new Promise<void>((resolve) =>
    setTimeout(async () => {
      console.log('Time-based revalidation 2');
      await getRandomNumForDataCache();
      resolve();
    }, 2000)
  );

  // This will hit stale cache
  await new Promise<void>((resolve) =>
    setTimeout(async () => {
      console.log('Time-based revalidation 3');

      // This request torrigers revalidation so it includes the revalidate option
      await getRandomNumForDataCache({ revalidate: 5 });
      resolve();
    }, 5000)
  );

  // This will hit cache revalidated by the above request
  await new Promise<void>((resolve) =>
    setTimeout(async () => {
      console.log('Time-based revalidation 4');
      await getRandomNumForDataCache();
      resolve();
    }, 1000)
  );

  return new Response('Time-based revalidation', { status: 200 });
}
src/app/api/data-cache/on-demand-revalidation.ts
import { getRandomNumForDataCache } from '@/utils/app-fetch';
import { revalidateTag } from 'next/cache';

export async function GET() {
  console.log('On-demand revalidation 1');
  await getRandomNumForDataCache({ tags: ['leech'] });

  console.log('On-demand revalidation 2');
  await getRandomNumForDataCache();

  revalidateTag('leech');

  console.log('On-demand revalidation 3');
  await getRandomNumForDataCache();

  return new Response('', { status: 200 });
}
src/app/api/data-cache/opt-out.ts
export async function GET() {
  console.log('Opt-out data cache 1');
  await optOut();

  console.log('Opt-out data cache 2');
  await optOut();

  console.log('Opt-out data cache 3');
  await optOut();

  return new Response('', { status: 200 });
}

const optOut = async () => {
  const res = await fetch('http://localhost:3005', { cache: 'no-store' });
  console.log(await res.text());
};
ダミー外部APIサーバー

こちらと同様にextenral-server.jsファイルを作成してください。

外部API、Next.Jsサーバーを起動

shell
node external-server.js

上記の外部APIサーバーを起動したShellとは別プロセスで起動してください。

shell
npm run dev

起動後、http://localhost:3000/data-cacheにアクセスします。

再起動とCacheクリア

.nect/cache/fetch-cacheに、Data Cacheが保持されるので、検証の度にfetch-cacheフォルダを手動で削除し、npm run devを実行してください。

image.png

通常のData Cache

Normalボタンを押下してください。

3回のRequest全てに同じ値が返ってきており、Cacheが利用されていることが分かります。

Next.Jsサーバーログ
Normal 1
130
Normal 2
130
Normal 3
130

Time-based Revalidation

再起動とCacheクリアをします。

Time-based revalidationボタンを押下してください。

1つ目のRequestでData Cacheが生成され、2、3つ目のRequestに対してCacheが返されています。

ただし、3つ目に関しては、revalidate : 5で設定した5秒が過ぎているので、Stale Cacheが返されており、裏でRevalidationが動き、Cacheが更新されます。

4つ目は、上記で更新されたCacheから値が返されるため、新しい値となっています。

Next.Jsサーバーログ
Time-based revalidation 1
611
Time-based revalidation 2
611
Time-based revalidation 3
611
Time-based revalidation 4
826

On-Demand Revalidation

再起動とCacheクリアをします。

On-demand revalidationボタンを押下してください。

1つ目のRequestで生成されたCacheが2つ目に利用されていることが分かります。

その後、revalidateTag('leech')により、Cacheが削除されるため、3つ目のRequestは外部APIサーバーから新たに値を取得しています。

Next.Jsサーバーログ
On-demand revalidation 1
123
On-demand revalidation 2
123
On-demand revalidation 3
114

Cacheを無効化

再起動とCacheクリアをします。

Opt-outボタンを押下してください。

Data Cacheを無効化しているため、全てのRequestが外部APIサーバーから値を取得しています。

Next.Jsサーバーログ
Opt-out data cache 1
664
Opt-out data cache 2
91
Opt-out data cache 3
249

最後に

Request Memoizationとの組み合わせで、最適な動きが実現されそうな気がしつつも、仕組みを知らないことで調査が難航するなんてこともありそうです。

ちなみに、今回初めて知ったのですが、Route HandlerもStatic Renderingの対象なんですね。奥が深いです。

10
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
10
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?