Next.js 13 の cache 周りを理解する - fetchCache
Next.js 13 App Router の cache 周りを理解したい記事シリーズです。
- Automatic fetch() Request Deduping
- revalidate
- fetchCache ← この記事
fetchCache
オプション
Route Segment Config - App Router では、Route Segment Configという仕組みで、レイアウトあるいはページから特定の変数を export すると動作をカスタマイズできます。前回の記事では、このオプションのひとつである revalidate
について動作を確認しました。
しかし、Route Segment Config には他にもキャッシュに関係するオプションがあります。
それが fetchCache
オプションです。
このオプションに関して、beta ドキュメント上には次の記載があります。
This is an advanced option that should only be used if you specifically need to override the default behavior.
DeepL 翻訳: これは高度なオプションであり、特にデフォルトの動作を上書きする必要がある場合にのみ使用する必要があります。
本当に必要な特別なケースでのみの利用を推奨されているようです。その前提を頭の片隅に残しつつ、実際にどういうオプションか見てみましょう。
fetch()
Dynamic Functions と fetchCache
オプションを理解するためには、Dynamic Functions と呼ばれる関数群と fetch()
の関係性を把握しておく必要があります。
Next.js 13 App Router では、Cookie や HTTP ヘッダ情報といった、リクエストスコープに紐づく情報を取得するための関数をいくつか提供しています。それらはドキュメント上では Dynamic Functions と呼ばれ、 cookies()
や headers()
といった関数が該当します。
そして、これらを利用した場合 fetch()
のデフォルトのキャッシュ挙動が変化します。
fetch()
はデフォルトでは cache:force-cache と同等の挙動で、すべてのクエリをキャッシュします。しかし、Dynamic Functions が呼ばれると、同一リクエスト内での以降の fetch()
は cache:no-store に切り替わり、キャッシュを利用しなくなります。(※fetch()
の cache オプションに関しては、別の記事でも解説しています。)
実際に確認してみます。ネストしたレイアウト・ページで fetch()
を実行しています。しかし、途中の Foo Layout
内での fetch()
実行前のタイミングで cookies()
を呼び出しています。
- Root Layout
- Foo Layout (
fetch()
前にcookies()
を呼び出し)- Bar Layout
- Bar Page
- Bar Layout
- Foo Layout (
import { cookies } from "next/headers";
export default async function Layout({ children }: { children: React.ReactNode }) {
// ★Dynamic Functions
cookies();
const res = await fetch("http://localhost:3001/sample-api?page=foo_layout");
const { data } = await res.json();
return (
<div className="bg-gray-200 p-4 border-2 border-gray-400 rounded">
Foo Layout : {data}
{children}
</div>
);
}
すると、次のような描画結果となります。(確認のためリロードしています)
cookies()
の呼び出しより上位階層ではキャッシュが利用されていないことがわかります。下位階層のコンポーネントから実行が開始され、今回のケースでは単純にfetch()
の呼び出しも下位階層のほうが先となります。
本記事のサンプルでは動作がわかりやすいよう、各コンポーネントでの非同期処理は単一の
fetch()
のみとしているため、単純に下位階層のコンポーネントから順にfetch()
が実行されます。しかし、実際のプロダクト開発では他の非同期処理が混ざったり、複数のfetch()
実行が含まれることも多く、実行順序は一概に保証されるものではないためご注意ください。※(@koichikさんにご指摘いただきました。ありがとうございます!)
これが fetchCache
理解の前提となる Dynamic Functions と fetch()
の挙動です。
fetchCache
オプションのデフォルト動作
さて、Dynamic Functions と fetch()
の関係を把握できましたが、この動作を制御するためのオプションが fetchCache
です。
fetchCache
には次のオプションを指定可能です。
'auto'
'default-cache'
'default-no-store'
'only-cache'
'only-no-store'
'force-cache'
'force-no-store'
ひとつずつオプションの挙動を確認してみましょう。
fetchCache
各オプションの説明
⚠️ 注意事項
Next.js 13.3 で手元で動作を確認しましたが、一部ドキュメントの通りに動作しないようです。
(コードも見てみましたが、不具合かもしれない...)
以下説明は、ドキュメント上での記述を解説したものであり、Next.js 13.3 以前のバージョンでは安定的に動作しない可能性があります。
現状では「将来的にこういうオプションが使えるかも」程度の温度感で見ていただくほうが良さそうです。
fetchCache = 'auto'
(default)
fetchCache に何も指定しなかった場合のデフォルトの挙動です。
-
fetch()
に指定された cache オプションを利用する -
fetch()
に cache オプションが無い場合、Dynamic Functions の実行前後で挙動が変化する- 実行前の場合は force-cache 相当
- 実行前の場合は no-store 相当
つまり、さきほどの Dynamic Functions と fetch()
の動作の正体は、この 'auto'
指定によるものでした。
整理すると次のようになります。
fetch() - cache オプション | Dynamic Functions | キャッシュの利用 |
---|---|---|
(指定なし) | なし | キャッシュする |
force-cache | なし | キャッシュする |
no-store | なし | キャッシュしない |
(指定なし) | 実行後 | キャッシュしない |
force-cache | 実行後 | キャッシュする |
no-store | 実行後 | キャッシュしない |
fetchCache = 'default-cache'
cache オプションを省略した場合、すべて force-cache 相当で動作させます。
-
fetch()
に指定された cache オプションを利用する -
fetch()
に cache オプションが無い場合、Dynamic Functions の実行に関わらず force-cache 指定となる
※太字は 'auto'
指定との差異
fetch() - cache オプション | Dynamic Functions | キャッシュの利用 |
---|---|---|
(指定なし) | なし | キャッシュする |
force-cache | なし | キャッシュする |
no-store | なし | キャッシュしない |
(指定なし) | 実行後 | キャッシュする |
force-cache | 実行後 | キャッシュする |
no-store | 実行後 | キャッシュしない |
Dynamic Functions の実行後でもデフォルトではキャッシュ対象になる点が 'auto'
との大きい差異ですね。
一方で、cache オプションの指定自体は可能なので、任意箇所で no-store でキャッシュを無効化することは可能です。
fetchCache = 'default-no-store'
cache オプションを省略した場合、すべて no-store 相当で動作させます。
-
fetch()
に指定された cache オプションを利用する -
fetch()
に cache オプションが無い場合、Dynamic Functions の実行に関わらず no-store 指定となる
※太字は 'auto'
指定との差異
fetch() - cache オプション | Dynamic Functions | キャッシュの利用 |
---|---|---|
(指定なし) | なし | キャッシュしない |
force-cache | なし | キャッシュする |
no-store | なし | キャッシュしない |
(指定なし) | 実行後 | キャッシュしない |
force-cache | 実行後 | キャッシュする |
no-store | 実行後 | キャッシュしない |
'default-cache'
がデフォルトを force-cache にするのに対して、その逆の動作になるイメージですね。
fetchCache = 'force-cache'
すべての fetch()
リクエストをすべてキャッシュ対象とします。fetch()
に no-store オプションが指定されたとしてもキャッシュ対象になります。
-
fetch()
に指定された cache オプションは利用されない -
fetch()
に cache オプションが無い場合、Dynamic Functions の実行に関わらず force-cache 指定となる
※太字は 'auto'
指定との差異
fetch() - cache オプション | Dynamic Functions | キャッシュの利用 |
---|---|---|
(指定なし) | なし | キャッシュする |
force-cache | なし | キャッシュする |
no-store | なし | キャッシュする |
(指定なし) | 実行後 | キャッシュする |
force-cache | 実行後 | キャッシュする |
no-store | 実行後 | キャッシュする |
fetchCache = 'force-no-store'
'force-cache'
の逆で、すべてをキャッシュしません。fetch()
に force-cache オプションが指定されたとしてもキャッシュされません。
-
fetch()
に指定された cache オプションは利用されない -
fetch()
に cache オプションが無い場合、Dynamic Functions の実行に関わらず no-store 指定となる
※太字は 'auto'
指定との差異
fetch() - cache オプション | Dynamic Functions | キャッシュの利用 |
---|---|---|
(指定なし) | なし | キャッシュしない |
force-cache | なし | キャッシュしない |
no-store | なし | キャッシュしない |
(指定なし) | 実行後 | キャッシュしない |
force-cache | 実行後 | キャッシュしない |
no-store | 実行後 | キャッシュしない |
とにかくキャッシュしません。
fetchCache = 'only-cache'
cache オプションを省略した場合、すべて force-cache 相当で動作させます。
また、cache オプションに no-store が指定された場合には fetch()
がエラーとなります。
-
fetch()
に cache オプションが無い場合、Dynamic Functions の実行に関わらず force-cache 指定となる -
fetch()
に指定された cache オプションが no-store の場合はエラーとなる
※太字は 'auto'
指定との差異
fetch() - cache オプション | Dynamic Functions | キャッシュの利用 |
---|---|---|
(指定なし) | なし | キャッシュする |
force-cache | なし | キャッシュする |
no-store | なし | (エラー) |
(指定なし) | 実行後 | キャッシュする |
force-cache | 実行後 | キャッシュする |
no-store | 実行後 | (エラー) |
fetchCache = 'only-no-store'
cache オプションを省略した場合、すべて no-store 相当で動作させます。
また、cache オプションに force-cache が指定された場合には fetch()
がエラーとなります。
'only-cache'
の逆ですね。
-
fetch()
に cache オプションが無い場合、Dynamic Functions の実行に関わらず no-store 指定となる -
fetch()
に指定された cache オプションが force-cache の場合はエラーとなる
※太字は 'auto'
指定との差異
fetch() - cache オプション | Dynamic Functions | キャッシュの利用 |
---|---|---|
(指定なし) | なし | キャッシュしない |
force-cache | なし | (エラー) |
no-store | なし | キャッシュしない |
(指定なし) | 実行後 | キャッシュしない |
force-cache | 実行後 | (エラー) |
no-store | 実行後 | キャッシュしない |
というわけで fetchCache オプションの紹介でした。
only-no-store
などは、決してキャッシュさせたくないことが明確なページなどで事故防止のために使えそうですね。
fetchCache
オプションは現状ではまだ動作が怪しいですが、App Router が正式なものになった際には活用できるかもしれません。
Discussion
「Dynamic Functions と fetch()」の
これはより正確には「下位階層のコンポーネントから実行が開始される」ですね
一方こちらはそうなるとは限りません
たとえば
page.tsx
が最初に実行を開始しますが、それがもし非同期なコンポーネントでfetch
を呼び出しているとレスポンスを受信するまで処理は中断しますするとその完了を待たずに上位階層のコンポーネント (
layout.tsx
) の実行が開始されますそれがまた非同期なコンポーネントであれば、やはりその完了を待たずに更に上位階層のコンポーネントの実行が開始され、、、
ということで記事中の例では最大4つのコンポーネントツリーが並行に実行 (レンダー) される可能性があります
さきほど別記事にコメントしたことにも関連しますが、並行にレンダーされるためにそれぞれのページやレイアウトの
fetch
がどのような順番で実行されるかは不確定になります (fetch
で呼び出すAPIのレスポンスタイムに左右される)その結果、あるDynamic Functions/fetchがどの
fetchCache: 'auto'
でデフォルトのfetch
に影響するかも不確定になります個人的には
auto
は使用禁止にしたいと思っちゃいました確かにそのほうが正しいですね。訂正しておきます🙏
すいませんこちら自分がまだ少し理解できていないかもなのですが、記事内でのサンプルのみを例にすると、単純にコンポーネントの構成が
となり、実行は下位階層からとなるため、それぞれに仕込んだ fetch() の呼び出し順序自体は Bar Page → Bar Layout → Foo Layout → Root Layout で固定になるのかなと思ったのですが、認識違うでしょうか?
それぞれの Layout や Page からまた別のコンポーネントツリーが生えたうえで、そこからfetch() が実行されていた場合には、それらを含めた実行順序は不確定になる、という理解をしています。
(このあたりは言われてみれば確かにという感じでした。ありがとうございます)
確かにそうですね..
実行ごとにキャッシュの有無が切り替わる可能性が出てくるので、問題発生時などに特定も困難になりそうですね。
記事内のサンプルそのままの話であれば確かに
fetch()
の呼び出し順は固定ですね (少なくとも今のNext.jsの実装では)それはそれぞれのコンポーネントが呼び出す
fetch()
が一つだけ、かつ唯一の非同期操作だからですしかし、Dedupeの記事でコメントしたように
fetch()
の前にsetTimeout()
を入れるだけでも呼び出し順は変わり得ますたとえば記事内のサンプルでBar Pageを
setTimeout()
で少し待機してからfetch()
を呼び出すようにすると、そのfetch()
は他のLayoutのfetch()
より後に実行される可能性があります (というか自分の手元では実際にそうなりました)それが前のコメントで
と書いた話になります
これは、たとえばBar Layoutに渡される
children
はBar Pageが返すReactElement
そのものではない、というのがポイントかなと思っていて、つまり (少なくとも直接的には)ではないということです
実際にBar Layoutに渡される
children
は別のコンポーネントツリーへの参照のようなもので、ReactElement
のtype
が"div"
などHTML要素を表す文字列でもコンポーネントを示す関数でもなく、Proxy
オブジェクトが設定されたものになっていますこの
Proxy
なReactElement
により、Bar LayoutはBar Pageの完了を待つことなく実行を開始することができるのだと理解しています同様にFoo LayoutはBar Layoutの完了を、Root LayoutはFoo Layoutの完了を待つことなくそれぞれ実行を開始することができます
つまり記事中のサンプルでは最大で4つのコンポーネントツリーが並行にレンダーされます
このように並行で実行されるため、それぞれのコンポーネントツリーにおける最初の非同期操作以外はどのような順序で実行されるかは不確定というのが前のコメントに書いたことになります
これはこの記事のサンプルへのコメントとしては少々行きすぎだったかもしれませんね
しかしながら、この記事の読者が (前のコメントをした時点のように) 下位層のコンポーネントから逐次的に実行されるというイメージを持ってしまうと、たとえばBar Pageが複数の
fetch()
を呼び出していた場合の挙動にビックリすることになるかもしれないので、ちょっと気になったのでしたありがとうございます!理解できたと思います。
おそらく私自身の認識はご説明頂いた内容と一致していたと思うのですが、記事内での説明では「今回のサンプルでは」という暗黙的なコンテキストが含まれていたので、常に下位レイヤから順に実行されてしまうような理解をされてしまう可能性がある文章になっていましたね。
そのあたり誤解を招かないよう、本文内に追記しておこうかと思います!
いえ、たしかにミスリードを招く可能性があった点かなと思ったので、ご指摘ありがたいです!