perf(boot): instant HTML splash in the shell for a faster perceived first paint

The app is a pure client-rendered SPA (ssr=false, adapter-static), so until the
JS bundle downloads, parses and mounts, the page is blank — only then does the
layout's own loading state appear. True route prerendering would mean turning on
SSR, an architectural change at odds with the SPA design.

Instead, bake a tiny boot splash into the shell (`app.html`): a centered spinner
that paints the moment the HTML is parsed — before the app bundle or its CSS
load — covering the blank gap. The root layout removes `#app-splash` the instant
it mounts (before `session.load`), so public routes like /login (which render
without waiting for the session) appear immediately and protected routes hand
off to their own loading UI. `light-dark()` plus early `color-scheme` rules make
the splash match the resolved theme (incl. the saved override), so there's no
colour flash when the app CSS arrives; it respects `prefers-reduced-motion`.

Pure HTML/CSS in the shell — no new requests, no JS framework on the critical
path, ~0.6 KB in index.html. Verified on the real static-dist build in headless
Chromium: the shell ships `#app-splash`, the layout removes it after mount, the
app renders, and there are no runtime errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M8Vb9QHmLZnEMzHz7MrFy6
This commit is contained in:
Claude
2026-06-19 16:36:54 +00:00
parent a8709b447a
commit a211d9d4a6
2 changed files with 49 additions and 0 deletions
+44
View File
@@ -23,9 +23,53 @@
}
})();
</script>
<!--
Instant boot splash. Paints on HTML parse (before the app bundle and its
CSS load), covering the otherwise-blank gap during JS download/parse for a
faster perceived first paint. The root layout removes `#app-splash` as soon
as it mounts. `light-dark()` + the early `color-scheme` rules match the
resolved theme so there's no colour flash when the app CSS arrives.
-->
<style>
html[data-color-scheme='dark'] {
color-scheme: dark;
}
html[data-color-scheme='light'] {
color-scheme: light;
}
#app-splash {
position: fixed;
inset: 0;
z-index: 9999;
display: grid;
place-items: center;
background: light-dark(#f5f7fa, #0f172a);
}
#app-splash__spinner {
width: 38px;
height: 38px;
border-radius: 50%;
border: 3px solid light-dark(#e2e8f0, #334155);
border-top-color: #ff5e3a;
animation: app-splash-spin 0.7s linear infinite;
}
@keyframes app-splash-spin {
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: reduce) {
#app-splash__spinner {
animation: none;
}
}
</style>
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div id="app-splash" role="status" aria-label="Loading">
<div id="app-splash__spinner"></div>
</div>
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>