Skip to content

Synchronous rapid navigation triggers full page reload instead of soft RSC navigation #796

@Divkix

Description

@Divkix

Problem

During truly synchronous rapid A→B→C navigation (both clicks fired in the same JavaScript execution tick), vinext triggers a full page reload instead of completing a soft RSC navigation.

Evidence

E2E test rapid-navigation.spec.ts was modified in PR #745 to verify no full page reload occurs during rapid navigation:

// Set marker to detect full page reload
await page.evaluate(() => {
  (window as any).__NAV_MARKER__ = "started-at-a";
});

// Click B then immediately click C (synchronously, same tick)
await page.evaluate(() => {
  const linkB = document.querySelector('[data-testid="page-a-link-to-b"]') as HTMLElement;
  const linkC = document.querySelector('[data-testid="page-a-link-to-c"]') as HTMLElement;
  if (linkB) linkB.click();
  if (linkC) linkC.click();
});

// Verify no full page reload happened (marker should survive)
const marker = await page.evaluate(() => (window as any).__NAV_MARKER__);
expect(marker).toBe("started-at-a");  // ❌ FAILS: marker is undefined

Result: __NAV_MARKER__ is undefined, indicating a full page reload wiped all JavaScript state.

Key Observations

  1. No error logged: No [vinext] RSC navigation error: appears in console, ruling out the catch block at app-browser-entry.ts:809 as the source.

  2. Works with sequential clicks: If the two clicks have even a tiny delay (sequential page.click() calls), navigation completes correctly.

  3. Same-route navigation passes: The same-route query change during cross-route navigation test passes with the same synchronous click pattern.

Difference Between Passing and Failing

Test isSameRoute Result
Same-route query change true ✅ Passes
Cross-route A→B→C false ❌ Full page reload

This suggests the issue is related to cross-route navigation behavior when isSameRoute = false.

Root Cause Hypothesis

When isSameRoute = false, the navigation uses useTransition = false in renderNavigationPayload(). Combined with truly synchronous clicks:

  1. Navigation B starts with isSameRoute = false
  2. Navigation C starts in the same JavaScript tick before B can await anything
  3. State management (activeNavigationId, pendingPathname, navigationSnapshotActiveCount) may have an edge case
  4. Something triggers window.location.href = href without logging an error

Potential trigger points for hard navigation without error logging:

  • app-browser-entry.ts:728 — redirect handling (unlikely, no redirect involved)
  • app-browser-entry.ts:809 — catch block (but no error is logged)
  • External URL detection in navigateClientSide() (unlikely, same-origin URLs)

Investigation Needed

  1. Add diagnostic logging: Temporarily add console.log before every window.location.href assignment to identify which code path triggers the reload.

  2. Check state at navigation start: Log activeNavigationId, pendingPathname, navigationSnapshotActiveCount when each navigation starts.

  3. Compare with Next.js behavior: Verify if Next.js handles synchronous rapid navigation differently.

Related

Workaround

The E2E test was updated to remove the marker check, allowing CI to pass while this is investigated. The test still covers rapid navigation but without verifying the no-reload invariant.

Files Affected

  • packages/vinext/src/server/app-browser-entry.ts — RSC navigation logic
  • packages/vinext/src/shims/link.tsx — Link click handler
  • packages/vinext/src/shims/navigation.ts — Client navigation state

Severity

High — Full page reload during what should be a smooth client-side navigation breaks the SPA experience and loses all client-side state.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions