Description
Cross-route client-side navigation (e.g. /top to /tv-shows) hangs indefinitely in Firefox. The old page stays visible and the new page never renders. Same-route navigations (e.g. changing searchParams on /top) work fine in all browsers.
This is Firefox-specific and only reproduces on production builds served via wrangler dev (workerd). Chrome works fine for all navigation types.
Root cause
React's startTransition never commits when the entire component tree is replaced during a cross-route navigation in Firefox. The transition render starts but React never calls the commit phase, so useLayoutEffect callbacks never fire and the navigation promise never resolves.
This is not a React bug. Next.js avoids this entirely because their segment-level cache architecture only swaps changed route segments inside startTransition -- parent layouts stay mounted, so the transition is always an incremental update. vinext replaces the full RSC tree on every cross-route navigation, which is a much larger update that React's transition scheduler apparently cannot finalize in Firefox.
Investigation trace (with debug logging in the browser entry):
- Link click handler fires correctly
navigateImpl is called
navigateRsc fetches the RSC response (200 OK, valid payload)
- Response is buffered and parsed successfully
renderNavigationPayload calls startTransition(() => setState({...}))
- Hangs here -- React never commits the transition, even after 30+ seconds
No React errors are reported via onUncaughtError or onRecoverableError callbacks on the root.
Proper fix
Implement segment-level caching (a CacheNode tree that mirrors route segments). On navigation, diff the old and new route trees and only swap segments that changed. This would make startTransition updates incremental (matching Next.js behavior) and eliminate the Firefox hang.
See analysis in #639 for details on how Next.js handles this via layout-router.tsx + segment-cache/navigation.ts.
Workaround
PR #643 works around this by detecting cross-route vs same-route navigations and only using startTransition for same-route navigations. Cross-route navigations use synchronous state updates, which commit immediately in all browsers:
const isSameRoute = url.pathname === window.location.pathname;
// ...
renderNavigationPayload(payload, snapshot, commitEffect, isSameRoute);
This means cross-route navigations lose the "keep old UI visible during loading" behavior, but the page navigates. Same-route navigations (filter changes) still benefit from transitions.
Reproduction
- Build any App Router app with multiple routes that have Suspense boundaries
- Serve via
wrangler dev
- Open in Firefox
- Click a link that navigates to a different route (different pathname)
- The page stays on the old route indefinitely
Environment
- Firefox (tested on latest)
- Production build on workerd via
wrangler dev
- Does NOT reproduce in Chrome
- Does NOT reproduce on Vite dev server (Node.js)
- Does NOT affect Next.js (segment-level updates avoid the problem)
Description
Cross-route client-side navigation (e.g.
/topto/tv-shows) hangs indefinitely in Firefox. The old page stays visible and the new page never renders. Same-route navigations (e.g. changing searchParams on/top) work fine in all browsers.This is Firefox-specific and only reproduces on production builds served via
wrangler dev(workerd). Chrome works fine for all navigation types.Root cause
React's
startTransitionnever commits when the entire component tree is replaced during a cross-route navigation in Firefox. The transition render starts but React never calls the commit phase, souseLayoutEffectcallbacks never fire and the navigation promise never resolves.This is not a React bug. Next.js avoids this entirely because their segment-level cache architecture only swaps changed route segments inside
startTransition-- parent layouts stay mounted, so the transition is always an incremental update. vinext replaces the full RSC tree on every cross-route navigation, which is a much larger update that React's transition scheduler apparently cannot finalize in Firefox.Investigation trace (with debug logging in the browser entry):
navigateImplis callednavigateRscfetches the RSC response (200 OK, valid payload)renderNavigationPayloadcallsstartTransition(() => setState({...}))No React errors are reported via
onUncaughtErrororonRecoverableErrorcallbacks on the root.Proper fix
Implement segment-level caching (a
CacheNodetree that mirrors route segments). On navigation, diff the old and new route trees and only swap segments that changed. This would makestartTransitionupdates incremental (matching Next.js behavior) and eliminate the Firefox hang.See analysis in #639 for details on how Next.js handles this via
layout-router.tsx+segment-cache/navigation.ts.Workaround
PR #643 works around this by detecting cross-route vs same-route navigations and only using
startTransitionfor same-route navigations. Cross-route navigations use synchronous state updates, which commit immediately in all browsers:This means cross-route navigations lose the "keep old UI visible during loading" behavior, but the page navigates. Same-route navigations (filter changes) still benefit from transitions.
Reproduction
wrangler devEnvironment
wrangler dev