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:
@@ -23,9 +23,53 @@
|
|||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
</script>
|
</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%
|
%sveltekit.head%
|
||||||
</head>
|
</head>
|
||||||
<body data-sveltekit-preload-data="hover">
|
<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>
|
<div style="display: contents">%sveltekit.body%</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -21,6 +21,11 @@
|
|||||||
let ready = $state(false);
|
let ready = $state(false);
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
|
// The instant HTML boot splash has done its job — the app is mounted, so
|
||||||
|
// the route (login renders immediately; protected routes show their own
|
||||||
|
// loading state) is already in the DOM behind it.
|
||||||
|
document.getElementById('app-splash')?.remove();
|
||||||
|
|
||||||
// Redirect old `#/...` bookmarks to the new path before anything else.
|
// Redirect old `#/...` bookmarks to the new path before anything else.
|
||||||
if (typeof location !== 'undefined' && location.hash.startsWith('#/')) {
|
if (typeof location !== 'undefined' && location.hash.startsWith('#/')) {
|
||||||
const mapped = hashUrlToPath(location.hash);
|
const mapped = hashUrlToPath(location.hash);
|
||||||
|
|||||||
Reference in New Issue
Block a user