iTranslated by AI
Verifying the Behavior of "use cache"
Introduction
While considering migrating a Next.js v15 application to Next.js v16, I needed to verify the behavior of "use cache" on Vercel, so I am writing this article as a side effect of that process.
Background
I was using unstable_cache because I didn't want to fetch data during build time, but I wanted it to be cached for a certain period after the first data fetch upon access. However, after upgrading to Next.js v16, the behavior changed, so I decided to switch to using "use cache" properly.
The official documentation alone is insufficient to fully understand the behavior of "use cache", and those aspects are being discussed in issues like the ones below. In particular, this comment in the first issue was very helpful for my understanding. In this article, I am verifying the behavior of "use cache" on Vercel while cross-referencing these comments. If there are any contradictions between this article and the discussions in the issues, please consider the issues to be correct.
Also, I have created a verification app, but if someone else purges the cache, you won't be able to confirm the exact behavior. If you want to check it properly, please deploy it to your own environment.
"use cache" Overview
First, I'd like to look back at what "use cache" was. This is already a well-covered topic, so please skip it or skim through if you already know.
To use the "use cache" feature, you need to switch the Next.js mode. Switching modes is simple; just add cacheComponents: true to your Next.js config.
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
// Add this
cacheComponents: true,
};
export default nextConfig;
Setting cacheComponents: true changes the existing Static/Dynamic specification method for page generation. By default, each page becomes Partial Prerender, and when "use cache" is specified at the top of the page, that page becomes Static. If a Dynamic Route or Dynamic API is used, it results in Partial Prerender. This is natural since the page must be generated using parameters that are only known at the time of access.
You can output cache logs by setting the environment variable NEXT_PRIVATE_DEBUG_CACHE=1.
Static
Pages or components with "use cache" at the top are rendered as Static at build time. However, since they are regenerated when the cache is purged, I think understanding them as being "cached" is closer to the reality than being truly static pages as the directive's name implies.
static/cached
Example build log
Route (app) Revalidate Expire
┌ ○ /
├ ○ /_not-found
├ ◐ /dynamic/component/[slug]
│ └ /dynamic/component/[slug]
├ ◐ /dynamic/data/[slug]
│ └ /dynamic/data/[slug]
├ ○ /static/cached 15m 1y
└ ◐ /static/uncached 15m 1y
○ (Static) prerendered as static content
◐ (Partial Prerender) prerendered as static HTML with dynamic server-streamed content
If there is a part you want to render dynamically and you try to wrap it in Suspense forcibly, it won't have any effect. It will be rendered at build time regardless.
Partial Prerender
First, what is Partial Prerender? The page itself is dynamically generated, and you can specify Static or Partial Prerender for each individual element or piece of data within it.
static/uncached
Example
For example, the following page.tsx does not have "use cache" specified, so it becomes a Partial Prerender page, and the page itself is rendered dynamically for each request. Cached and CachedRemote have "use cache" specified within their respective components, so these parts are cached, while the Uncached component is not cached and is generated dynamically for each request. Dynamic components must be wrapped in Suspense to specify a fallback during rendering.
export default async function Page() {
return (
<PageLayout>
<Cached />
<CachedRemote />
<Suspense fallback={<LoadingCard />}>
<Uncached />
</Suspense>
</PageLayout>
);
}
interface Props {
params?: Promise<{ slug: string }>
}
export async function Cached({ params }: Props) {
"use cache"
const { slug } = params ? await params : {}
cacheTag(`cached-component-${slug}`, "component", "all")
await wait(5)
const date = new Date()
return (
...
)
}
export async function CachedRemote({ params }: Props) {
"use cache: remote"
const { slug } = params ? await params : {}
cacheTag(`cached-remote-component-${slug}`, "component", "all")
await wait(5)
const date = new Date()
return (
...
)
}
export async function Uncached({ params }: Props) {
const { slug } = params ? await params : {}
await wait(5)
const date = new Date()
return (
...
)
}
Summary
To summarize briefly, when you switch modes with cacheComponents: true, it works as follows:
- All components will either be cached or dynamically generated. (The default is dynamic)
- Cached components are pre-rendered at build time.
- Dynamically generated components must be wrapped in
Suspense.
- Within a
Partial Prerenderpage, you can choose whether to cache or dynamically generate each individual element.
Dynamic Routing
This is the main topic of this article. If you want to cache components within Dynamic Routing based on the understanding summarized above, the version where parameters are not passed will be cached as follows:
export default async function Page({
params,
}: PageProps<"/dynamic/component/[slug]">) {
return (
<PageLayout>
{/* When params are not passed */}
<Cached />
{/* When params are passed */}
<Suspense fallback={<LoadingCard />}>
<Cached params={params} />
</Suspense>
</PageLayout>
)
}
interface Props {
params?: Promise<{ slug: string }>
}
async function Cached({ params }: Props) {
"use cache"
const { slug } = params ? await params : {}
return (
...
)
}
As expected, the component where params are not passed is cached and rendered immediately, while the component where params are passed triggers rendering every time.
dynamic/component/cache/1

So, what should we do if we want to cache rendering results or data that use Dynamic APIs? There might be use cases where parameters are unknown at build time or you don't want to render them during the build, but you want to cache the results once an access occurs. In this case, you will use "use cache: remote".
"use cache: remote"
First, let's look at the results. Components with "use cache" are rendered every time, just like before, but components with "use cache: remote" are cached as expected.
dynamic/component/1

Reading the documentation for "use cache: remote", it says the following:
The 'use cache: remote' directive lets you declaratively specify that a cached output should be stored in a remote cache instead of in-memory. While this gives you more durable caching for specific operations, it comes with tradeoffs: infrastructure cost and network latency during cache lookups.
It seems using "use cache: remote" means it will be cached remotely rather than in-memory. If you read further, it's also explained here when it should be used.
Remote caching provides the most value when content is deferred to request time (outside the static shell). This typically happens when a component accesses request values like
cookies(),headers(), or searchParams, placing it inside a Suspense boundary.
It is most effective when you want to cache content at request time—in other words, exactly what we want to achieve.
Where to `await`
When you want to cache rendering results or data fetched using Dynamic Routing params or other Dynamic APIs, the placement of await can be tricky. Specifically, there are two patterns:
- Wrapping everything in
Suspenseand passing resolved values to the cached component. - Passing the
Promiseas-is and resolving it inside the component.
In the documentation example, the Promise is resolved at the top of the component within Suspense before passing the value to the caching component. However, this structure waits for the rendering of the entire page to complete, failing to leverage the benefits of Partial Prerender for partial rendering.
dynamic/component/awaited/1

When implementing this pattern, ensure you resolve the Promise within the Suspense that wraps the cached component.
<Suspense fallback={<LoadingCard />}>
{params.then(({ slug }) => (
<Cached slug={slug} />
))}
</Suspense>
In contrast, if you pass params to the component as a Promise and extract the value inside using const ... = await params, it provides fallbacks per component and eliminates the need to wrap the whole page in Suspense. Therefore, this approach is generally better.
dynamic/component/1

Differences between "use cache" and "use cache: remote"
So, what is the difference between "use cache" and "use cache: remote"? This comment was very helpful regarding this part.
Here is an explanation of that comment.
First, as a premise, the behavior of "use cache" and "use cache: remote" seems to differ depending on the deployment environment.
By default, both "use cache" and "use cache: remote" use memory as storage for caching. Therefore, they should behave the same way when running next start locally after building.
Local execution
Let's look at the same page as dynamic/component/1, which was built and run locally and also deployed to Vercel. While the component with "use cache" was not cached when deployed to Vercel, it is cached locally, and as expected, both "use cache" and "use cache: remote" exhibit the same behavior.

Third-party hosting providers can customize the behavior of "use cache" and "use cache: remote" to provide cache behavior integrated with their platform. Vercel is no exception and has its own customization. It also seems possible to create new directives like "use cache: xxx".
Since Vercel is a serverless environment, even if data is stored in memory during the first access, it cannot be referenced during subsequent accesses because they may run on a different server. Consequently, in-memory caching becomes meaningless.
For this reason, applications on Vercel do not cache anything in memory.
"use cache" is solely used to detect whether an element is pre-renderable and whether it's possible to prefetch for immediate navigation.
Elements with "use cache: remote" are cached in external storage and referenced from each serverless environment. This makes it possible to cache elements even within Dynamic Routing.
That is the reason why "use cache" does not cache elements within Dynamic Routing on Vercel, whereas "use cache: remote" makes caching possible. By the way, while it's mentioned that "use cache: remote" incurs costs, it seems to be available to some extent as it works in the verification app running on Vercel's hobby plan. I'm not entirely sure which usage metric it relates to, but I suspect it might be something like ISR Write/Read.
Summary
Since this was a verification of behavior on Vercel, I recommend testing it yourself before using it in other environments. I have implemented most patterns in the verification app, so feel free to use it.
Also, I didn't investigate it specifically as I didn't need to use it, but there is also a feature called "use cache: private" that allows caching on the client side. I'm sure someone else will explain that eventually.
Discussion
検証および執筆ありがとうございます!
余計なお世話かと思いますが、少し気になるところがあるのでコメントさせていただきます
本記事の本題は「Dynamic Routingにおけるコンポーネントのキャッシュ」とのことですが、実際に書かれている内容はDynamic Routesを含めたDynamic APIsはほとんど (少なくとも直接的には) 関係ないと思います
本記事の検証結果は
"use cache"ではなく"use cache: remote"を使うと要約できるかと思います
実際、「
"use cache"と"use cache: remote"の違い」の「ローカル実行」に書かれているように、Dynamic Routesであってもローカル環境では"use cache: remote"と同じように"use cache"もキャッシュされていますよね従ってVercel環境においてもDynamic Routesであることはキャッシュされてない直接の要因ではないと思うのです
それではDynamic Routesの
paramsを使っていない場合 (「Without Parameters」) の<Cached>コンポーネントはなぜVercel環境でもキャッシュされているように見えるかというと、これは"use cache"ではなくPPRによるStatic Shellがキャッシュされているためだと思いますPPRと
"use cache"は (加えてDynamicIOやSegment Cacheも) Cache Componentsとして統合されて一つのフラグでまとめて有効になりますが、元々は独立に開発されてきたものですたとえば https://cache-test-silk.vercel.app/dynamic/component/1 を直接開いた場合のレスポンスHTMLを見ると、「Without Parameters」にある
<Cached>コンポーネントの部分は以下のようにレンダリングされていますこの部分はPPRによるStatic Shellです (ローカル環境で
next buildすると.next/server/app/dynamic/component/[slug].htmlに生成されます)"use cache"が指定された<Cached>コンポーネントだけでなく、"use cache"が指定されていないページコンポーネントの一部 (<h2>Without Parameters</h2>など) も含まれていることがわかりますStatic Shellは
"use cache"等の有無にかかわらず、コンポーネントを実行することなくレスポンスとして返せる部分 (<Suspense>の外側) です(余談かつご存じかもですが、Static Shellでは
<Suspense>の内側はフォールバックがレンダリングされた状態になってます)つまり、
<Cached>コンポーネントは ("use cache"が指定されていないページコンポーネントと同様に) Vercel環境ではキャッシュされていませんが、その実行結果を含むStatic Shellはキャッシュから返されるため、あたかも"use cache"が効いているように見えているだけだと思うのですこれを確認するには、たとえばページコンポーネントの「Without Parameters」のところを
のように
<Cached>を<Suspense>で囲みますすると、この
<Cached>はStatic Shellに含まれなくなるため、paramsが渡されていないにもかかわらず (Vercel環境では) コンポーネントとしてはキャッシュされないことが確認できるかと思います (毎回フォールバックが約5秒表示される)一方、
"use cache: remote"を指定した<CachedRemote>はこの場合でもキャッシュされるはずですVercel環境におけるコンポーネントレベルのキャッシュには
paramsやDynamic APIsを使うかどうかは影響がなく、"use cache"と"use cache: remote"のどちらを使っているかだけが影響することを確認できると思いますここまでのまとめとして、
"use cache"はキャッシュされない"use cache: remote"を使う"use cache"/"use cache: remote"とは独立しているとなると思います
ここからもう少しややこしくなるのですが、PPRはキャッシュされたStatic Shellをレスポンスの一部として素早く返すわけですが、その裏ではStatic Shellに含まれる部分、すなわちレイアウトコンポーネントやページコンポーネント、それらの子コンポーネント等も (
"use cache"等で実際にキャッシュされている場合を除き) 実行されますつまり、https://cache-test-silk.vercel.app/dynamic/component/1 の「Without Parameters」にある
<Cached>コンポーネント (Vercel環境ではキャッシュされていない) も実はリクエストの度に実行されているはずなのですそしてその実行結果はレスポンスHTMLの後ろの方に出てくるRSCペイロードに含まれます
culr等で https://cache-test-silk.vercel.app/dynamic/component/1 にアクセスすると、レスポンスが途中で約5秒止まる様子を見ることができます
そして約5秒後に送られてくるレスポンス (Static Shellよりも後ろの部分) には「Without Parameters」の
<Cached>の実行結果も含まれています (時刻の文字列で検索すると見つけられます)<Cached>コンポーネントにconsole.log()などを仕込んだとしてVercel環境で見れるのかどうか知りませんが (Vercelは使ったことがないのです😅)、もし見ることができるなら該当のURLにアクセスする毎に<Cached>の実行ログは「Without Parameters」側と「With Parameters」側で2回ずつ出力されるはずです (もし可能であれば検証して頂けると嬉しいです)Next.jsのキャッシュはCache Componentsで従来より洗練されたと思いますが、それでも
"use cache"("use cache: remote"等も含めた総称) の違い"use cache"と"use cache: remote"の違いなどなど難しい部分がまだありますね
長いコメント失礼しました
コメントありがとうございます!PPRの動作までは考慮できていませんでした。
いくつか追加で検証をしたのですが、考察までできていないので、結果だけお返しさせていただきます。
logの追加
各コンポーネントのレンダリングごとにログを出すように変更し、dynamic/component/1にアクセスしてみました。
結果として
"use cache"のコンポーネントでログが2回出ることはなく、1回分だけでした。みにくいかもしれないので、テキストログ
Suspenseの追加パラメーターを渡さないコンポーネントを
Suspenseで囲ったdynamic/component/suspended/1を新しく作成してアクセスしてみました。結果、リロードごとに5秒待つことはなくすぐ表示される状態でした。(リロードは
shift+cmd+r)検証ありがとうございます!
なんと…… 全く想定外の結果でした
つまりこの記事が正しくてDynamic APIsを使っていなければ
"use cache"はVercel環境であってもキャッシュされるということなんですね……Vercelで試すこともなく推測でコメントしてしまい申し訳ありませんでした
しかし、これだとローカル環境とVercel環境の挙動の違いを
cacheHandlers.defaultの実装だけで説明するのは難しい気がしますcacheHandlers.defaultからDynamic APIsの呼び出しの有無を取得できるようなAPIになっているようには見えないので……