Next.js Discord

Discord Forum

Next.js cacheComponents + <Link> ignores the static build cache on first navigation

Unanswered
German Shorthaired Pointer posted this in #help-forum
Open in Discord
German Shorthaired PointerOP
I've been banging my head against this for days and can't find a single clear answer online... not on the Next.js docs, not from ChatGPT, not from Google. Posting here again as I didn't get an answer previously.

---

## My setup

- Next.js 16 with cacheComponents: true (PPR enabled)
- Vercel deployment
- Supabase as database
- use cache with cacheLife("max") and cacheTag("tools") on all data fetching functions
- Pages come out as Static or Partial Prerender after npm run build

## The problem

After npm run build, my pages are fully prerendered or static. When I navigate to one of them for the first time using Next.js <Link>, I can see a cache MISS log fire in my server console — meaning it's completely ignoring the prerendered static build, spinning up a new server-side execution, and hitting Supabase fresh. Subsequent navigations to the same page are fine, but that first hit always misses.

## The insane part

When I replace <Link> with a plain <a> tag, the page loads instantly with zero server calls and zero Supabase hits. The prerendered HTML is served directly from Vercel's CDN, exactly as it should be. So <a> is simultaneously faster, cheaper, and actually respects the build cache — while <Link>, the officially recommended approach, bypasses all of it.

## My question

Why does <Link> with cacheComponents: true maintain a completely separate cache from the static build output? These pages are Static — fully rendered at build time — so why does client-side navigation re-execute the component on the server instead of serving the prerendered page?

And practically: why would I ever prefer <Link> over <a>if it only makes it slower by not taking the build pages into consideration?

28 Replies

so link prefetches right? do you think that the prefetch from the link is missing becuase it cached? @German Shorthaired Pointer just a shot in the dark
@Patrick MacDonald so link prefetches right? do you think that the prefetch from the link is missing becuase it cached? <@750783923617005629> just a shot in the dark
German Shorthaired PointerOP
I intentionally set it to false because it can lead to unexpectedly high Vercel bills esp in my case with lots of products/categories
hmmmmm
I wish I had more for you
@German Shorthaired Pointer I've been banging my head against this for days and can't find a single clear answer online... not on the Next.js docs, not from ChatGPT, not from Google. Posting here again as I didn't get an answer previously. --- ## My setup - Next.js 16 with `cacheComponents: true` (PPR enabled) - Vercel deployment - Supabase as database - `use cache` with `cacheLife("max")` and `cacheTag("tools")` on all data fetching functions - Pages come out as Static or Partial Prerender after `npm run build` ## The problem After `npm run build`, my pages are fully prerendered or static. When I navigate to one of them for the first time using Next.js `<Link>`, I can see a cache MISS log fire in my server console — meaning it's completely ignoring the prerendered static build, spinning up a new server-side execution, and hitting Supabase fresh. Subsequent navigations to the same page are fine, but that first hit always misses. ## The insane part When I replace `<Link>` with a plain `<a>` tag, the page loads instantly with zero server calls and zero Supabase hits. The prerendered HTML is served directly from Vercel's CDN, exactly as it should be. So `<a>` is simultaneously faster, cheaper, and actually respects the build cache — while `<Link>`, the officially recommended approach, bypasses all of it. ## My question Why does `<Link>` with `cacheComponents: true` maintain a completely separate cache from the static build output? These pages are `○` Static — fully rendered at build time — so why does client-side navigation re-execute the component on the server instead of serving the prerendered page? And practically: why would I ever prefer `<Link>` over `<a>`if it only makes it slower by not taking the build pages into consideration?
Maybe I’m mistaken, but using a plain <a> element triggers a full document navigation, meaning the browser performs a standard request and the server returns a complete HTML response (which may be SSR, SSG, or prerendered depending on the route configuration) and proly browser cached (try devtools to avoid caching responses).
In contrast, using Link leverages the Next.js client router, which performs a client-side transition. In the App Router, this results in fetching a React Server Components (RSC) payload via the React Flight protocol, which is then used to update the React tree on the client without reloading the page. The payload is also cached in memory for the duration of the session to optimize subsequent navigations.
m still trying to understand the interaction with fresh data and caching. Having PPR (Partial Prerendering) enabled doesn’t necessarily mean that all content is part of the static shell—individual segments can still be dynamic and streamed.
I’m currently debugging an issue where a revalidation triggered by one cached component causes other components to revalidate as well, even though each component appears to cache different data sources.
One thing that helped me debug this was creating a small component that displays the last render timestamp, so I can observe when specific segments actually re-render. I’d recommend doing something similar to better understand when the data layer is being cached
Also, using a single cache tag like "tool" for all data seems risky. If you’re relying on cache components to achieve granular caching, grouping everything under a single tag effectively creates a broad invalidation scope, which defeats the purpose of fine-grained cache control.
The only content that is part of the static shell has to exist on the layout or the page outside of suspense. Anything inside of suspense will not be part of the static shell
Anything dynamic will not be part of the static shell
@Patrick MacDonald Anything dynamic will not be part of the static shell
cached parts inside a suspense arent part of the shell?
If you use cache you don't need suspense
Use cache, remote and private. Require suspense but use cache itself. Does not
@Patrick MacDonald If you use cache you don't need suspense
but you need suspense to set the boundary of the cache
or im wrong?
Not with use cache
The data is cached so it doesn't need to be suspended
Just remove the suspense and check out the behavior
Because when you use cache, the data is known at build time
Remove the suspense and run a build and confirm that the page is static
It might be a little bit different because you're caching the component, but if you cache just the data fetch you don't need suspense on the component. I know that for sure
but all you see in build is partial prerender it doesnt show what part is static
Okay so for now comment out the dynamic parts. Leave the cashed part run a build and see if you get a static page
If only the cashed component is on the page and we've killed all the other data fetches the page should be static. If it does, then you know the cached data is part of the static shell
ok well can we continue in my open question if you dont mind?
Sure, let's hear it