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

1人フロントエンドAdvent Calendar 2024

Day 21

【Next.js】unstable_cacheの代わりに推奨しているuse cacheについて

Posted at

はじめに

Next.jsのバージョン14では非同期関数の結果をData Cacheに保存する関数としてunstable_cacheが用意されていました。

const data = unstable_cache(
  // データを取り出す非同期関数
  getData,
  // キャッシュを識別するキー
  ['data'],
  {
    // キャッシュをまとめて無効にするデータのタグ
    tags: ['user-data'],
    // 与えられた秒数が経過した後のデータ取得後にデータを再検証する
    // 未定義かfalseだとrevalidateTagやrevalidatePathをしない限りデータを再検証しない
    revalidate: 3600,
  },
)();

Next.jsのバージョン15ではunstable_cacheが廃止され、use cacheディレクティブがその代わりを担うようになりました。
use cacheはどのような働きをしてくれるのでしょうか。この記事ではuse cacheの働きについて解説します。

Data Cache

Data CacheはNext.jsが行う4つのキャッシュのうちの1つです。
他のキャッシュには、リクエスト単位に行われるRequest Memorization、ビルド時に行われるFull Route Cache、クライアント側で行われるClient-side Router Cacheがあります。

Data Cacheは異なるリクエストやデプロイ間で共有される永続的なキャッシュです。説明ではfetchData Cacheについての関係について考えます。後ほどここで紹介したものとuse cacheData Cacheとの関係が対応します。

以下のようにfetchoptions.cacheforce-cacheにしたデータ取得では、Data Cacheにデータがあればそのまま返し、なければ新たにデータを取得してData Cacheへの保存とリクエスト先への返却を行います。

fetch(url, { cache: 'force-cache' });

Next.js15からはoptions.cacheを指定しない時のfetchは他の非同期関数と同じように振る舞います。後述しますが、use cacheを使う世界では、fetchoptions.cacheによるキャッシュの管理は必要ないかも知れません。

再検証

キャッシュされたデータは2種類の方法でパージされます。

1つ目はfetchoptions.next.revalidateを与えた時です。

fetch(url, { next: { revalidate: 60 } });

上記のようにoptions.next.revalidateを60に設定した場合、最後にデータを取り出して60秒経過してから行われたデータ取得を最後にキャッシュが更新されます。
60秒経過するとキャッシュが自動的に削除されるのではなく、60秒経過後のデータの取得が終わったタイミングでData Cacheを更新することに注意が必要です(stale-while-revalidateのような動きです)。

// 最初のデータ取得 データの返却+データをData Cacheに保存
fetch(url, { next: { revalidate: 60 } });

// 60秒以内のデータ取得 Data Cacheから結果を返す
fetch(url, { next: { revalidate: 60 } });

// さらに60秒以上経過してからデータ取得
// Data Cacheから結果が返される+Data Cacheを削除して新しいデータに更新
fetch(url, { next: { revalidate: 60 } });

// 1つ前のfetchで更新されたデータをData Cacheから返す
fetch(url, { next: { revalidate: 60 } });

2つ目はrevalidatePathrevalidateTagを用いた時です。
revalidatePathrevalidatePath('/dashboard')のように引数に再検証させたいパスを渡して実行します。この操作は対象のパスで利用しているData Cacheを全て削除します。次のリクエストでデータの取得とData Cacheへのキャッシュが行われます。
revalidateTagrevalidateTag('user')のように使います。引数に渡された文字列はタグで、そのタグを持つData Cacheを全て削除します。Data Cacheのタグはfetchoptions.next.tagsで渡せます。

fetch(url, { next: { tags: ['user', 'blog'] } });

キャッシュしない

fetchの結果をキャッシュしたくない場合はoptions.cacheno-storeにします。

fetch(url, { cache: 'no-store' });

このようにした場合、Next.jsはData Cacheを確認せずに常に新たに取得した結果を返しますし、その結果をData Cacheに保存しません。この場合もRequest Memorizationは行われることに注意してください。

options.cacheがデフォルトの場合とno-storeを指定した場合の違いはルートが動的になるかどうかにあります。no-storeの場合は動的ルートになり常にデータを取得しますが、デフォルトの場合は他の要素によって動的ルートにならない限りはFull Route Cacheを返すのでデータの取得を行いません。

use cache

fetchと合わせてData Cacheがどのようなものかを紹介しました。
次に、use cacheを用いたData Cacheの取り扱いについて紹介します。

Next.jsの最新のバージョンが15.1の時にこの記事を作成しました。
use cacheはバージョン15.1にてcanaryとされています。動作の変更によってこの記事に誤った記述が生じることに注意してください。

use cacheを使う場合はまず、next.config.tsexperimental.dynamicIOtrueにします。

next.config.ts
import type { NextConfig } from 'next'
 
const nextConfig: NextConfig = {
  ...
  experimental: {
    ...
    dynamicIO: true,
  },
}
 
export default nextConfig

dynamicIOtureにすると、use cacheのスコープにないすべての非同期関数を利用する箇所が事前レンダリングから除外されます。パフォーマンスの低下に気をつけましょう。

use cacheuse serverと同じようにファイルの先頭やコンポーネント・関数の記述の先頭で宣言します。

use cacheが反映されたスコープは、すべての非同期関数がData Cacheとやりとりします(手動でオプトアウトしない限り)。つまり、そのスコープの非同期関数はすべてfetchoptions.cacheforce-cacheを付与したときと同じような動きをします。

use cacheスコープ内で呼ばれた非同期関数は、その入力値等を元にキャッシュキーを自動で作成します。キャッシュキーを構成する値はシリアライズ可能なものだけで、シリアライズできない値はキャッシュキーに含まれないです。キャッシュヒットについての不具合等につながるので注意が必要です。
また、非同期関数の戻り値はシリアライズされてData Cacheへ保存されるのでシリアライズ可能な値である必要があります。

use cacheはファイルに対して、以下のように付与します。

'use cache';

export default async function Page() {
  const data = await getData();
  // ...
}

このように宣言したファイルにネストされた全てのコンポーネントはuse cacheを継承します。
use cacheを付与されたファイルのコンポーネントは、すべてのデータのやり取りをData Cacheを通じることを仮定できるのでSuspenseなしで非同期コンポーネントを配置できます。

逆にSuspenseuse cacheの境界となり、Suspense内のコンポーネントは動的にデータ取得を行います(childrenも境界になります)。

// without-suspense/page.tsx
'use cache';

export default async function PageWithoutSuspense() {
  return (
    <div>
      <UserData />
    </div>
  );
};

// with-suspense/page.tsx
'use cache';

export default async function PageWithSuspense() {
  return (
    <div>
      <Suspense fallback={<UserDataFallback />}>
        <UserData />
      </Suspense>
    </div>
  );
};

// user-data.tsx
export const UserData = () => {
  const user = await getUser();
  return ...
}

上記の例のWithoutSuspensePageではUserDataのデータ取得でData Cacheへデータを問い合わせて取得するのに対して、WithSuspensePageUserDataのデータ取得で常に新しいデータを取得します。

コンポーネントは以下のように利用します。

export const CacheComponent = () => {
  'use cache';
  const data = await getData();

  return ...
};

そのコンポーネント内で実行されるすべての非同期関数の結果がData Cacheに保存されます。異なる場所からこのコンポーネントを再利用する場合も、生成されるキャッシュキーに違いがない場合は同じキャッシュが利用されます。

関数は以下のように利用します。

export const getData = async () => {
  'use cache';
  const data = await fetch(url);
  return data;
};

fetchであればfetch(url, { cache: 'force-cache' })のようにし、それ以外であればunstable_cacheを使うようにする必要がなくなり、同じuse cacheで管理できるようになりました。

再検証

Data Cacheで再検証を行う方法は2つありました。use cacheでもそれは同じです。

1つ目の再検証を行うまでの時間を定義する方法はcacheLifeを利用します。
cacheLifefetchoptions.next.revalidateのように秒数を渡すのではなく、secondsminuteshoursdaysweeksmaxの決められた値を渡します。
それぞれの値は以下のように動きます。

value Stale Revalidate Expire
デフォルト undefined 15分 無限
seconds undefined 1秒 1秒
minutes 5分 1分 1時間
hours 5分 1時間 1日
days 5分 1日 1週間
weeks 5分 1週間 1ヶ月
max 5分 1ヶ月 無限

Staleはクライアント側でデータをFreshとみなす期間で、これを超えるとキャッシュを削除して次の実行からサーバーへデータの確認を行います。
Revalidateはサーバーがデータのキャッシュの期限が切れていないことを確認する期間です。
Expireはキャッシュが有効切れするまでの期間を指します。

2つ目の再検証であるrevalidatePathrevalidateTagで行う方法はそのまま使えます。
再検証の目印となるタグをはcacheTagで付与します。

const getUsers = async () => {
  'use cache';
  const users = await getUsers();
  cacheTag('users', 'user');
  for (const user of users) {
    cacheTag(`user-${user.id}`);
  }
  return users;
};

この場合getUserusersuseruser-id・・・がタグとして紐づけられます。

キャッシュしない

非同期関数がなくても対象の場所で動的に実行させたい場合はconnectionを使います。

import { connection } from 'next/server'
 
export default async function Page() {
  await connection()
  ...
}

connectionから先はすべてプレレンダリングから除外されます。これもcanaryの機能なので利用には気をつけましょう。

おわりに

Data Cacheuse cacheについて紹介しました。
Next.jsバージョン14の世界ではfetchとその他の非同期関数でキャッシュする方法が異なり大変でしたが、use cacheの出現によりfetchを他の非同期関数と同じように扱えるのでそれが解決されとても便利になりました。

これまでの動的ルートと静的ルートで物事を考えていると概念が混ざってわかりにくくなるので、別のモデルとして考えた方が理解しやすそうだなと思いました。

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