Skip to content

fix: GPU-promote the blog sticky header to kill the iOS WebKit nav flash (#610)#636

Merged
vivek7405 merged 1 commit into
mainfrom
fix/ios-sticky-header-compositor-promotion
Jun 19, 2026
Merged

fix: GPU-promote the blog sticky header to kill the iOS WebKit nav flash (#610)#636
vivek7405 merged 1 commit into
mainfrom
fix/ios-sticky-header-compositor-promotion

Conversation

@vivek7405

Copy link
Copy Markdown
Collaborator

Root cause (now confirmed, not guessed)

#610 reproduces only on iOS (both Safari and Chrome on iOS, which are both WebKit), and is fine on desktop and Android. That is the signature of a WebKit position: sticky repaint bug, corroborated by deep research (web + the locally cloned frameworks):

  • WebKit bugs 226532 / 276465 / 280316: during a scroll-driven layer-position recompute, a sticky/fixed element's repaint rect is not cleared, leaving a stale background for a frame. Matches the "background flashes" symptom exactly.
  • Apple dev forums 705172 and muffinman.io: the proven fix is compositor-layer promotion (translateZ(0) / translate3d(0,0,0)) on the sticky element.
  • The locally cloned frameworks confirm why only webjs hits this: webjs's partial-swap router preserves the sticky header while swapping <main> and scrolling to 0. Turbo full-swaps the body (header recreated); Next scrollIntoViews the new segment; Astro snapshots via View Transitions. None preserves a sticky header through a content-swap + scroll-to-0, so none triggers the bug.

This is why the four prior attempts (backdrop-blur, opacity, scroll-behavior, the data-navigating gating) all failed: they targeted the header's paint inputs, not WebKit's repaint path. #622 attempted compositor promotion but I closed it untested while chasing the wrong theory.

Fix

Promote the header to a stable GPU layer, on the sticky element itself (a transform on an ancestor breaks sticky in Safari):

.site-header {
  transform: translateZ(0); -webkit-transform: translateZ(0);
  backface-visibility: hidden; -webkit-backface-visibility: hidden;
}

A static translateZ (cheaper than a permanent will-change) gives the header its own layer so the scroll-time recompute can't leave a stale background.

Built-in A/B control (one deploy confirms it)

The header is preserved across a soft nav, so the treatment set on the full page load persists through the forward navigation under test:

  • Load / = fix ON.
  • Load /?nofix = fix OFF (reproduces the flicker).

Navigate forward (scroll the post list, tap a post) from each, on the iPhone. If / is clean and /?nofix flashes, the fix and the diagnosis are both confirmed.

Verification

  • Local SSR: / renders class="site-header ...", /?nofix renders class="site-header nofix ...", translateZ(0) present. webjs check clean.
  • The flicker itself is iOS-WebKit-only (invisible to desktop, headless, emulation), so on-device confirmation is the gate.

Follow-up (after on-device confirmation)

Document the iOS sticky-header gotcha + the GPU-promotion pattern in agent-docs/styling.md / lit-muscle-memory-gotchas.md, and remove the ?nofix control. Held until confirmed so we do not document an unverified fix.

Re #610.

https://claude.ai/code/session_01W8RLiSnkwKXDmnkoasQfZF

#610 reproduces ONLY on iOS (both Safari and Chrome, both WebKit), never on
desktop or Android. That is the signature of a WebKit position:sticky
repaint bug: during a client-router forward nav the in-place content swap
plus the instant scroll-to-top drives a scroll-time layer-position recompute
that fails to clear the sticky header's repaint rect, so its background
flashes for one frame (WebKit bugs 226532 / 276465 / 280316; corroborated by
Apple dev forums 705172 and muffinman.io). The header is preserved across
the swap, which is unique to webjs's partial-swap router (Turbo full-swaps,
Next scrollIntoViews the new segment), so no other framework hits this.

The proven fix is compositor-layer promotion ON the sticky element (never an
ancestor, which breaks sticky in Safari): a static translateZ(0) plus
backface-visibility:hidden and the -webkit- variants give the header a
stable GPU layer that skips the bad repaint path. A static translateZ is
cheaper than a permanent will-change.

Ships with a `?nofix` A/B control: the header is preserved across a soft
nav, so loading `/` (fix on) vs `/?nofix` (fix off) lets the flicker be
confirmed and reproduced on-device in a single deploy.

Re #610.

Claude-Session: https://claude.ai/code/session_01W8RLiSnkwKXDmnkoasQfZF
@vivek7405 vivek7405 marked this pull request as ready for review June 19, 2026 15:36
@vivek7405 vivek7405 merged commit 8a755bb into main Jun 19, 2026
9 checks passed
@vivek7405 vivek7405 deleted the fix/ios-sticky-header-compositor-promotion branch June 19, 2026 15:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant