nextjs - 💡(How to fix) Fix `router.refresh()` eagerly refetches every in-viewport `<Link>` in v16 (was lazy in v15) — ~200x peak ISR Writes in production [1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
vercel/next.js#93210Fetched 2026-04-25 06:02:26
View on GitHub
Comments
1
Participants
2
Timeline
7
Reactions
1
Author
Participants
Timeline (top)
labeled ×2subscribed ×2commented ×1issue_type_added ×1

Code Example

pnpm install
cd apps/v15    && pnpm next build && pnpm next start   # http://localhost:3001
cd apps/v16    && pnpm next build && pnpm next start   # http://localhost:3000
cd apps/canary && pnpm next build && pnpm next start   # http://localhost:3002

---

Operating System:
  Platform: darwin
  Arch: arm64
  Version: Darwin Kernel Version 25.3.0: Wed Jan 28 20:51:28 PST 2026; root:xnu-12377.91.3~2/RELEASE_ARM64_T6041
  Available memory (MB): 131072
  Available CPU cores: 16
Binaries:
  Node: 24.15.0
  npm: 11.12.1
  Yarn: N/A
  pnpm: 10.33.1
Relevant Packages:
  next: 16.2.4 // Latest available version is detected (16.2.4).
  eslint-config-next: N/A
  react: 19.2.5
  react-dom: 19.2.5
  typescript: 6.0.3
Next.js Config:
  output: N/A
RAW_BUFFERClick to expand / collapse

Link to the code that reproduces this issue

https://github.com/sanity-io/nextjs-repro-router.refresh-v16-regression

To Reproduce

The repo contains three apps that render the same @repo/ui blog, differing only by Next.js version:

apps/v16 and apps/canary have identical config; the only difference is that [email protected] enables experimental.prefetchInlining by default.

No local setup is required — the three apps are deployed on Vercel. Do the following for each deployment:

  1. Open the controller (root) URL in one tab:
  2. Open /blog on the same origin in one or more additional tabs. Each blog tab listens on BroadcastChannel for a message from the controller and reacts with router.refresh(). Scroll each blog tab so a handful of post <Link>s are inside the viewport and have been prefetched.
  3. In the blog tab, open DevTools → Network. Filter to prefetch requests (the RSC / prefetch requests to the App Router) and make the x-vercel-cache column visible (right-click column header → Response Headers → x-vercel-cache).
  4. Switch to the controller tab and click Call revalidateTag() (or Call updateTag() on v16 / canary). The controller runs the revalidation and then broadcasts a router.refresh() to every open blog tab.
  5. In the blog tab's Network panel, count the prefetch requests that fire and look at their x-vercel-cache values.

To run locally instead (<Link> prefetching is disabled in next dev, so you must use next build && next start):

pnpm install
cd apps/v15    && pnpm next build && pnpm next start   # http://localhost:3001
cd apps/v16    && pnpm next build && pnpm next start   # http://localhost:3000
cd apps/canary && pnpm next build && pnpm next start   # http://localhost:3002

Current vs. Expected behavior

Expected (v15 behavior)

After router.refresh(), prefetched <Link>s currently in the viewport are marked stale but not refetched until the user signals intent (hover / touch) or the link leaves and re-enters the viewport. Lazy.

On v15 (https://nextjs-refresh-v15-reference.sanity.dev/blog), that's exactly what happens: the refresh re-fetches the current page, but no extra <Link> prefetch requests fire.

Current (v16 behavior — the regression)

On v16 (https://nextjs-refresh-v16-regression.sanity.dev/blog), every <Link> currently in the viewport fires a fresh prefetch immediately, one request per route segment, and every one of them comes back with x-vercel-cache: REVALIDATED. Each of those is an ISR Write on Vercel. Eager.

On canary (https://nextjs-refresh-canary.sanity.dev/blog), the default experimental.prefetchInlining collapses the per-segment requests into one per link, so you see fewer prefetches than on v16 — but they're still x-vercel-cache: REVALIDATED. The eager behavior is unchanged.

Production impact (www.sanity.io)

After upgrading www.sanity.io from v15 to v16 in production:

  • Daily average ISR Writes before upgrade: 7.1M units.
  • Daily average ISR Writes after upgrade: 358.5M (~50x).
  • Single-day peak: 1.52B (~200x).
  • ISR read/write ratio inverted: ~3 reads : 1 write → ~1 read : 5 writes.

Disabling <Link> prefetching made the spike go away, which is what pointed us at prefetching behavior. We worked around it by no longer calling router.refresh() on content changes in production and by switching from revalidateTag(tag, { expire: 0 }) to revalidateTag(tag, 'max') so the cache entry is marked CACHE: STALE instead of CACHE: REVALIDATED. That disables the live content-push UX on the public site — which was the whole point of wiring router.refresh() to a push channel.

The two v16 changes that compound

  1. Per-segment prefetching (documented): a single <Link> now issues multiple prefetch requests, one per route segment. experimental.prefetchInlining (default in canary) partially mitigates this by inlining unchanged segments.
  2. router.refresh() is now eager with prefetched links (undocumented): on v15 refresh marked links stale; on v16 every in-viewport prefetched link is refetched immediately, for every segment.

The only reference to (2) we've found is under Partial Prerendering (PPR) in the v16 prefetching guide:

Data invalidations (revalidateTag, revalidatePath) silently refresh associated prefetches — https://nextjs.org/docs/app/guides/prefetching#partial-prerendering-ppr

That wording suggests this only applies when cacheComponents: true. In practice this repo reproduces the eager viewport refetching regardless of whether cacheComponents is enabled.

What we'd like

An opt-out that restores v15's lazy behavior: after router.refresh() (or a revalidateTag that invalidates a prefetch), in-viewport <Link>s are marked stale but not refetched until the user shows intent or the link re-enters the viewport. Arguably this should be the default; eager is only safe for sites with a small audience, small link surface, or no router.refresh()-on-event pattern.

Provide environment information

Operating System:
  Platform: darwin
  Arch: arm64
  Version: Darwin Kernel Version 25.3.0: Wed Jan 28 20:51:28 PST 2026; root:xnu-12377.91.3~2/RELEASE_ARM64_T6041
  Available memory (MB): 131072
  Available CPU cores: 16
Binaries:
  Node: 24.15.0
  npm: 11.12.1
  Yarn: N/A
  pnpm: 10.33.1
Relevant Packages:
  next: 16.2.4 // Latest available version is detected (16.2.4).
  eslint-config-next: N/A
  react: 19.2.5
  react-dom: 19.2.5
  typescript: 6.0.3
Next.js Config:
  output: N/A

Which area(s) are affected? (Select all that apply)

cacheComponents, Linking and Navigating, Partial Prerendering (PPR), Performance

Which stage(s) are affected? (Select all that apply)

Vercel (Deployed), next start (local)

Additional context

  • First canary that introduced this: we haven't bisected to a specific canary yet — happy to, if it'd help. The behavior is present on [email protected] (latest stable) and on [email protected].
  • Reproducible both on next start locally and on Vercel. The x-vercel-cache: REVALIDATED signal is only visible on the deployed version (set by Vercel's edge), but the prefetch request counts reproduce on next start as well.
  • Deployment platform: Vercel.
  • The reproduction uses BroadcastChannel + router.refresh() to stand in for an SSE / WebSocket / Sanity Live push channel; any mechanism that ends in router.refresh() after a revalidateTag exhibits the same behavior.

extent analysis

TL;DR

The most likely fix is to opt-out of the eager prefetching behavior introduced in Next.js 16 by waiting for an official fix or workaround from the Next.js team.

Guidance

  • Investigate the experimental.prefetchInlining option to see if it can mitigate the issue, although it may not completely restore the v15 behavior.
  • Consider disabling <Link> prefetching or switching from revalidateTag(tag, { expire: 0 }) to revalidateTag(tag, 'max') as temporary workarounds.
  • Monitor the Next.js issue tracker and documentation for updates on this regression and potential fixes.
  • If possible, bisect the canary versions to identify the specific change that introduced this behavior, which may help the Next.js team provide a more targeted fix.

Example

No code example is provided as the issue is related to the behavior of Next.js and not a specific code snippet.

Notes

The issue is specific to Next.js 16 and later versions, and the behavior is not documented in the official Next.js documentation. The provided workarounds may have performance implications and should be carefully evaluated before implementation.

Recommendation

Apply a workaround, such as disabling <Link> prefetching or using revalidateTag(tag, 'max'), until an official fix is available from the Next.js team. This is because the eager prefetching behavior can have significant performance implications, especially for large-scale applications.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING