-
Notifications
You must be signed in to change notification settings - Fork 271
Description
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
navigateImplis callednavigateRscfetches the RSC response (200 OK, valid payload)- Response is buffered and parsed successfully
renderNavigationPayloadcallsstartTransition(() => 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)