Skip to content

dogfood: iOS back-swipe gesture flashes a blank page (client router) #641

@vivek7405

Description

@vivek7405

Problem

On a real iPhone, the interactive edge back-SWIPE gesture flashes a blank page for a few milliseconds before the previous page appears. The back BUTTON works fine. It is iOS-only: reproduces in both Safari and Chrome on iOS (both WebKit), and does NOT happen on Android or desktop.

This is distinct from the now-fixed #610 sticky-header flicker (that was the forward-nav header background; this is a whole-page blank on the back gesture).

Design / approach

Likely a router paint/snapshot-timing issue. During the interactive back-swipe, iOS renders a native snapshot of the previous page sliding in. But webjs is an SPA: it navigates via history.pushState, sets history.scrollRestoration = 'manual', and swaps the DOM on popstate. There is a window where the native snapshot is gone but the swapped-in DOM has not painted yet, so the page reads blank.

Candidate fixes to A/B on-device:

  • Defer the popstate swap to a requestAnimationFrame so WebKit paints at a frame boundary (the timing Turbo Drive uses for its render). An on-device flag window.__webjsDiag.raf was added then removed in fix: use position:fixed for the blog header to end the iOS nav flicker (#610) #640, but it was only evaluated against the FORWARD flash, not isolated against the back-swipe, so re-test it specifically here.
  • Restore from the in-memory snapshot synchronously / earlier so content is painted before the gesture settles.
  • Reconsider whether scrollRestoration = 'manual' interacts with the native gesture snapshot.

Implementation notes (for the implementing agent)

  • Where to look in packages/core/src/router-client.js: the popstate listener (onPopState, registered ~L184), history.scrollRestoration = 'manual' (~L203), and the popstate path in performNavigation (the isPopState cached-snapshot branch, snapshotGet then applySwap ~L855, then window.scrollTo).
  • Landmines: this is iOS-WebKit-only and invisible to desktop, DevTools emulation, and headless, so it MUST be verified on a real iOS device. The forward-nav fix (fix: use position:fixed for the blog header to end the iOS nav flicker (#610) #640, position:fixed header) does NOT address this. The raf lever was already written and reverted (see chore: on-device isolation flags for the #610 iOS nav repaint (diagnostic) #637/fix: use position:fixed for the blog header to end the iOS nav flicker (#610) #640 history) so the diff is small to re-introduce.
  • Invariants: packages/ is plain .js + JSDoc, no .ts (AGENTS.md). The change must not regress the back/forward scroll-restoration UX (snapshot cache + manual scrollRestoration).
  • Tests + docs: a browser test should at least assert the popstate swap has painted content before the handler yields (the headline confirmation is manual on-device). If behavior changes, update agent-docs/advanced.md (client-router section).

Acceptance criteria

  • On a real iOS device (Safari + Chrome), the back-swipe gesture shows the previous page with no blank flash
  • The back BUTTON, forward nav, and back/forward scroll restoration are unchanged (no regression)
  • Android + desktop behavior unchanged
  • A browser test asserts the popstate swap paints content before yielding (counterfactual where feasible)
  • agent-docs/advanced.md updated if the client-router behavior changed

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type
No fields configured for issues without a type.

Projects

Status
Todo

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions