fix(pwa): strip first-paint compositing layers to prevent iOS WebKit freeze#223
Conversation
…freeze
The blank-on-cold-start is a whole-page compositing freeze in iOS-26 WKWebView
(Edge/Chrome on iOS); Safari works because system WebKit handles the compositing,
while WKWebView is more sensitive to first-paint compositing-layer pressure. A
compositing freeze can't be detected from JS (FCP is recorded but never composited
to screen), so the Paint-Timing auto-reload couldn't fire — pivot from recover to
prevent by reducing what's composited at first paint.
Before any JS runs (the freeze is at the consent gate), two elements force
compositing layers:
- #map { isolation: isolate } — a full-screen composited layer, empty until the
map inits. Removed: isolation only contained the hillshade mix-blend-mode:multiply,
which blends against the opaque base tiles beneath it regardless (multiply against
the white body behind a full-screen map is a no-op), so it's visually safe.
- static #offline-banner (position:fixed + transform) — moved out of the static
HTML and lazy-created in updateOfflineBanner() only when the app goes offline, so
it's absent from first paint.
If this stops the freeze, first-paint compositing pressure was the trigger and we
can reintroduce containment more surgically if ever needed.
Closes #222
Refs #218, #220
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Code Review — PR #223: strip first-paint compositing layersOverall: Well-reasoned, focused fix. The root cause analysis (WKWebView compositing sensitivity at first paint) is sound, and both changes are independently justified and harmless if the hypothesis turns out to be wrong. What the PR does
CorrectnessOffline banner lazy-creation logic is correct. The three-branch structure handles all cases:
CSS Minor observations
Test coveragePer CLAUDE.md, vitest covers pure functions only (no DOM/Leaflet integration). The DOM mutation logic in SummaryNo issues found. The logic is sound, edge cases are handled, and the two changes are justified independently of the iOS fix (the lazy banner is a minor DOM hygiene improvement; removing 🤖 Generated with Claude Code |
Summary
Pivot from recover to prevent on the iOS-26 WKWebView whole-page render freeze. A compositing freeze can't be detected from JS (FCP is recorded but never composited to screen), so the Paint-Timing auto-reload (#221) never fired — and Safari works while only Edge/WKWebView freezes, which points at WKWebView's greater sensitivity to first-paint compositing-layer pressure. So: reduce what's composited at first paint.
Before any JS runs (the freeze is at the consent gate), two elements force compositing layers:
Changes
src/style.css— removeisolation: isolatefrom#map(a full-screen composited layer present from first paint). Visually safe:isolationonly contained the hillshade overlay'smix-blend-mode: multiply, which blends against the opaque base tiles directly beneath it regardless (and multiply against the whitebodybehind a full-screen map is a no-op).index.html+src/main.ts— remove the static#offline-banner(position: fixed+transform) from the HTML; lazy-create it inupdateOfflineBanner()only when the app actually goes offline, so it's absent from first paint.Test plan
type-check(app + worker),lint,test(96),build— all greenisolationin CSS, no static#offline-bannerin HTML, lazy-create present in bundleNotes
The Paint-Timing auto-reload from #221 stays as a harmless fallback. Once this is confirmed, the temporary diagnostics can be stripped and the (exonerated) service worker simplified.
Closes #222
Refs #218, #220
🤖 Generated with Claude Code