Skip to content

Cross-route client navigation hangs in Firefox (startTransition never commits) #652

@NathanDrake2406

Description

@NathanDrake2406

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):

  1. Link click handler fires correctly
  2. navigateImpl is called
  3. navigateRsc fetches the RSC response (200 OK, valid payload)
  4. Response is buffered and parsed successfully
  5. renderNavigationPayload calls startTransition(() => setState({...}))
  6. 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

  1. Build any App Router app with multiple routes that have Suspense boundaries
  2. Serve via wrangler dev
  3. Open in Firefox
  4. Click a link that navigates to a different route (different pathname)
  5. 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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions