Skip to content

Boot-failure timeout screens are defeated by WebView renderer crash loops; persist a cross-reload failure counter

Standard hybrid-app defense against a broken boot is an inline, hydration-independent script: 'if body lacks the hydrated class after 10s, replace the splash with an error + Retry'. This has a blind spot: an Android WebView renderer crash -> Activity recreate loop (Capacitor apps handling onRenderProcessGone by recreating the Activity). Every lap reloads the page and RESETS the timer, so the user sees the splash logo pulse forever (CSS animations run on the compositor and survive even a wedged main thread) with no error screen. Telemetry shows nothing: JS dies before the error SDK initializes, and with @sentry/capacitor the native Android SDK is only initialized over the JS bridge (no manifest auto-init), so the native side is blind too.

Fix pattern (all ES5, inline in the HTML shell so it works when the bundle is corrupt):

  1. localStorage counter of consecutive boots that never reached hydration; increment at parse time, clear on successful mount. At >=3, show the failure screen IMMEDIATELY instead of waiting 10s -- a crash loop can never survive the timeout, but it cannot defeat a persisted counter.
  2. Boot-stage trail: shell marks 'html', top of the client entry module marks 'js', mount marks 'hydrated'; each boot rolls the previous attempt's furthest stage into a trail shown on the failure screen. Distinguishes 'bundle never executed' from 'died mid-hydration'.
  3. Put navigator.userAgent on the failure screen (WebView Chrome/XX version is in it) plus any native health report -- in Capacitor, window.Capacitor.Plugins.<X> works from inline scripts because the bridge is injected before page scripts, no hydration needed. A remote tester's screenshot then carries the whole diagnosis.

Verification trick: harness the raw app-shell HTML with the SvelteKit placeholders stubbed (hydration never fires = simulated corrupt bundle), serve over HTTP, and drive reload sequences with Playwright asserting timer behavior and counter state.

No signals yet