nextjs - ✅(Solved) Fix Pages Router hydration causes an extra client render on full page load when any rewrite is configured, including non-rewritten routes [1 pull requests, 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#93091Fetched 2026-04-22 07:42:56
View on GitHub
Comments
1
Participants
2
Timeline
4
Reactions
0
Author
Participants
Timeline (top)
labeled ×2commented ×1issue_type_added ×1

Root Cause

That issue was closed as expected behavior because rewrites may require a post-hydration query update for static pages. This reproduction shows a narrower problem: the same extra rerender also happens on /, even though / is not rewritten.

PR fix notes

PR #93129: fix(#93091): Compute Pages Router rewrite reconciliation before router creation

Description (problem / solution / changelog)

What?

Fix Pages Router rewrite hydration so the initial client router state is decided per request instead of falling back to the app-global “rewrites exist somewhere” path.

This introduces an explicit rewrite reconciliation state with three outcomes:

  • required: the initial route/query snapshot does not match the reconciled snapshot, so the hydration reconciliation update still has to run
  • not-required: the initial payload already matches the reconciled snapshot, so the extra hydration reconciliation update can be skipped
  • unknown: the client cannot prove either safely, so the current conservative fallback behavior is preserved

The implementation now:

  • computes exact request-time reconciliation on the server when request-local rewrite knowledge exists
  • serializes only exact request-time answers into __NEXT_DATA__
  • intentionally omits that field for build-time prerendered/shared HTML
  • computes reconciliation before createRouter(...) so the router can start with the correct initial isReady
  • keeps client-side narrowing restricted to a deterministic safe subset of rewrites:
    • internal destinations only
    • no has
    • no missing

Why?

Today, Pages Router hydration falls back to the rewrite resync path whenever rewrites exist anywhere in the app, even for requests where:

  • no rewrite matched
  • a rewrite matched but the initial payload already reflects the final route/query state

That creates unnecessary extra hydration work and delayed initial isReady in cases that can now be decided exactly.

At the same time, the existing CDN/static HTML constraint still has to hold:

  • for statically optimized rewrite targets, /rewritten and /target can serve the same shared HTML file
  • that shared build-time HTML cannot carry a per-request “this request was rewritten” marker in baked __NEXT_DATA__

This change addresses the bug more directly without violating that constraint:

  • exact request-time answers are reused only when the server really has them
  • shared prerendered HTML still does not embed request-local rewrite state
  • non-deterministic rewrites still stay conservative and remain unknown

It also follows the earlier direction discussed in #38422: make the relevant initial router state correct before router creation, rather than relying on the old post-hydration correction path for every rewrite-bearing app.

How?

  1. Add a new Pages Router rewrite reconciliation helper flow that compares:

    • the initial serialized route/query snapshot
    • the reconciled route/query snapshot after rewrite resolution
  2. Compute exact request-time reconciliation in the Pages handler when the request actually flowed through an internal rewrite.

  3. Serialize only exact request-time answers (required / not-required) into __NEXT_DATA__.

    • unknown is intentionally omitted
    • build-time prerendering also omits the field entirely
  4. Before createRouter(...), reuse any exact serialized answer when one exists. Otherwise, resolve rewrites on the client and narrow only within a deterministic safe subset.

  5. Pass the resulting reconciliation state into the router constructor so initial readiness becomes:

    • not-required -> ready immediately
    • required -> delayed until hydration reconciliation runs
    • unknown -> existing conservative delayed-ready fallback
  6. Add direct unit and integration coverage for all three states:

    • required
    • not-required
    • unknown

Examples covered by the tests:

  • /rewrite-to-gsp -> /gsp?foo=bar => required
  • /rewrite-to-gsp-not-required -> /gsp => not-required
  • /rewrite-to-gsp-unsafe -> /gsp?foo=bar with missing => unknown

Fixes #93091 Related to #38422

Changed files

  • packages/next/errors.json (modified, +2/-1)
  • packages/next/src/build/webpack/plugins/build-manifest-plugin-utils.ts (modified, +32/-0)
  • packages/next/src/client/index.tsx (modified, +30/-2)
  • packages/next/src/lib/load-custom-routes.ts (modified, +6/-0)
  • packages/next/src/server/base-server.ts (modified, +10/-4)
  • packages/next/src/server/render.tsx (modified, +128/-7)
  • packages/next/src/server/request-meta.ts (modified, +10/-0)
  • packages/next/src/server/route-modules/pages/pages-handler.ts (modified, +33/-10)
  • packages/next/src/server/server-utils.test.ts (modified, +100/-1)
  • packages/next/src/server/server-utils.ts (modified, +49/-1)
  • packages/next/src/shared/lib/router/router.ts (modified, +11/-1)
  • packages/next/src/shared/lib/router/utils/resolve-rewrites.ts (modified, +18/-0)
  • packages/next/src/shared/lib/router/utils/rewrite-reconciliation.test.ts (added, +301/-0)
  • packages/next/src/shared/lib/router/utils/rewrite-reconciliation.ts (added, +414/-0)
  • packages/next/src/shared/lib/utils.ts (modified, +2/-0)
  • test/integration/router-is-ready/next.config.js (added, +24/-0)
  • test/integration/router-is-ready/test/index.test.ts (modified, +21/-0)
  • test/integration/router-rerender/next.config.js (modified, +31/-8)
  • test/integration/router-rerender/pages/blocking-gsp/[slug].js (added, +19/-0)
  • test/integration/router-rerender/pages/gsp.js (added, +23/-0)
  • test/integration/router-rerender/pages/index.js (modified, +1/-1)
  • test/integration/router-rerender/test/index.test.ts (modified, +83/-6)

Code Example

pnpm install
pnpm build
pnpm start

---

Operating System:
  Platform: darwin
  Arch: arm64
  Version: Darwin Kernel Version 25.3.0: Wed Jan 28 20:54:55 PST 2026; root:xnu-12377.91.3~2/RELEASE_ARM64_T6031
  Available memory (MB): 65536
  Available CPU cores: 16
Binaries:
  Node: 22.6.0
  npm: 10.8.2
  Yarn: 1.22.22
  pnpm: 10.18.1
Relevant Packages:
  next: 16.3.0-canary.2 // Latest available version is detected (16.3.0-canary.2).
  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/gurkerl83/next-pages-router-global-rewrite-rerender-repro

To Reproduce

  1. Clone the linked reproduction.
  2. The reproduction configures exactly one rewrite: /rewritten -> /target.
  3. Run:
pnpm install
pnpm build
pnpm start
  1. Open / directly in the browser, or reload /.
  2. Open /rewritten directly in the browser, or reload /rewritten.
  3. Check the browser console.

Observed result:

  • _app logs two client renders on /
  • _app logs two client renders on /rewritten

Important notes:

  • This does not reproduce in next dev.
  • Client-side navigation with <Link> is not the repro path here.
  • The extra render happens during the initial browser hydration path on a full page load or reload.
  • The rendered pages do not use useRouter().
  • The on-page render counters use plain React refs.
  • _app reads Router.router only for logging.

Current vs. Expected behavior

Current behavior:

In the linked reproduction, the app has one rewrite: /rewritten -> /target.

On a full page load or reload in production:

  • /rewritten gets an extra client render
  • / also gets an extra client render, even though / is not rewritten

Expected behavior:

I am not asking to remove rewrite-aware hydration reconciliation entirely.

I would expect the extra Pages Router hydration rerender to happen only when the current request actually depends on rewrite-derived route or query reconciliation.

So in this reproduction, I would expect:

  • /rewritten: rewrite-specific reconciliation if needed
  • /: a single client render

Provide environment information

Operating System:
  Platform: darwin
  Arch: arm64
  Version: Darwin Kernel Version 25.3.0: Wed Jan 28 20:54:55 PST 2026; root:xnu-12377.91.3~2/RELEASE_ARM64_T6031
  Available memory (MB): 65536
  Available CPU cores: 16
Binaries:
  Node: 22.6.0
  npm: 10.8.2
  Yarn: 1.22.22
  pnpm: 10.18.1
Relevant Packages:
  next: 16.3.0-canary.2 // Latest available version is detected (16.3.0-canary.2).
  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)

Pages Router, Linking and Navigating

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

next build (local), next start (local)

Additional context

I verified this on the latest canary: 16.3.0-canary.2.

This seems related to the older closed issue:

That issue was closed as expected behavior because rewrites may require a post-hydration query update for static pages. This reproduction shows a narrower problem: the same extra rerender also happens on /, even though / is not rewritten.

The reproduction is intentionally minimal:

  • one rewrite: /rewritten -> /target
  • / is static and not rewritten
  • the rendered pages do not use useRouter()
  • _app only logs Router.router state for debugging

So the issue here is not just “rewritten routes rerender”, but that the presence of any rewrite appears to trigger the hydration rerender path for unrelated static routes too.

What I think should happen instead:

  • keep rewrite-aware hydration reconciliation for requests that actually need it
  • avoid the extra hydration rerender for requests whose effective route identity and query are already correct

This reproduction is meant to show that the current trigger appears to be app-global, while the behavior should be request-local.

extent analysis

TL;DR

The issue can be addressed by refining the rewrite configuration to minimize unnecessary hydration rerenders for unrelated static routes.

Guidance

  • Review the Next.js documentation on rewrites and hydration to understand how they interact with static routes.
  • Consider adding a custom getStaticProps method to the static pages to control the hydration process.
  • Investigate the possibility of using router.asPath instead of Router.router to log the current route, as this might reduce the number of unnecessary rerenders.
  • Check if upgrading to a newer version of Next.js (if available) resolves the issue, as the reproduction is using a canary version (16.3.0-canary.2).

Example

No specific code example is provided due to the complexity of the issue and the need for a more detailed understanding of the Next.js configuration and the specific requirements of the application.

Notes

The issue seems to be related to the global nature of the rewrite configuration, which is triggering unnecessary hydration rerenders for unrelated static routes. A more detailed analysis of the Next.js configuration and the application's specific requirements is needed to provide a definitive solution.

Recommendation

Apply a workaround by refining the rewrite configuration and using custom getStaticProps methods to control the hydration process, as upgrading to a newer version of Next.js may not be immediately available or may not resolve the issue.

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