perf: paint branded loading screen from static HTML (0.85.0)

the <div id=root> was empty until the JS bundle downloaded and React mounted,
so the page was blank for ~2.4s (FCP). put the branded loading screen (pulsing
gem + 'amerc') directly in the static HTML, with its critical CSS inlined in
<head>, so it paints as soon as the CSS loads instead of waiting for JS.

- React's createRoot replaces #root on mount; the Suspense fallback renders the
  identical markup, so the static->React handoff is seamless (no flicker)
- inlined .app-loading critical CSS mirrors styles.css exactly + dark body bg
  so there's no white flash during load
- verified live: FCP 2.4s -> 1.4s, performance 86 -> 90, TBT 50 -> 0ms;
  app mounts cleanly, static loader replaced, 0 console errors
This commit is contained in:
artheru 2026-06-10 12:09:50 +08:00
parent 522ce6ec83
commit 5e0eeb79c6
3 changed files with 16 additions and 4 deletions

View File

@ -4,8 +4,20 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#0e0a14" /> <meta name="theme-color" content="#0e0a14" />
<style>
/* Critical CSS: paint the branded loading screen instantly from static
HTML, before the JS bundle loads & React mounts. React replaces #root
on mount; the Suspense fallback renders the same markup, so it's
seamless. Mirrors .app-loading in styles.css. */
html, body { margin: 0; background: #07060c; }
.app-loading { position: fixed; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 16px; background: radial-gradient(circle at 50% 38%, #160d22, #07060c 70%); }
.app-loading-gem { width: 28px; height: 28px; background: linear-gradient(135deg, #5fe6ff, #1f86c4); box-shadow: 0 0 0 3px #07273a, 0 0 22px rgba(39, 216, 255, 0.5); animation: app-pulse 1.3s ease-in-out infinite; }
.app-loading-word { font-family: Georgia, "Times New Roman", serif; color: #f3d27a; font-size: 18px; letter-spacing: 2px; opacity: 0.85; }
@keyframes app-pulse { 0%, 100% { transform: rotate(45deg) scale(1); opacity: 0.7; } 50% { transform: rotate(45deg) scale(1.16); opacity: 1; } }
@media (prefers-reduced-motion: reduce) { .app-loading-gem { animation: none; transform: rotate(45deg); } }
</style>
<meta name="description" content="amerc is the agent mercenary tavern — hire, run, publish, and remotely deliver AI agents across software boundaries. No infrastructure, just skills." /> <meta name="description" content="amerc is the agent mercenary tavern — hire, run, publish, and remotely deliver AI agents across software boundaries. No infrastructure, just skills." />
<meta name="version" content="0.84.0-quality" /> <meta name="version" content="0.85.0-fastpaint" />
<link rel="icon" href="/favicon.svg" type="image/svg+xml" /> <link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<link rel="icon" href="/app-192.png" type="image/png" sizes="192x192" /> <link rel="icon" href="/app-192.png" type="image/png" sizes="192x192" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" /> <link rel="apple-touch-icon" href="/apple-touch-icon.png" />
@ -56,7 +68,7 @@
</script> </script>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"><div class="app-loading"><span class="app-loading-gem" aria-hidden="true"></span><span class="app-loading-word">amerc</span></div></div>
<script type="module" src="/src/main.jsx"></script> <script type="module" src="/src/main.jsx"></script>
</body> </body>
</html> </html>

View File

@ -1,6 +1,6 @@
{ {
"name": "amerc-site", "name": "amerc-site",
"version": "0.84.0-quality", "version": "0.85.0-fastpaint",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {

View File

@ -10,7 +10,7 @@ import { api } from './api.js';
const Menu = ({ size = 20 }) => (<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" aria-hidden="true"><line x1="3" y1="6" x2="21" y2="6" /><line x1="3" y1="12" x2="21" y2="12" /><line x1="3" y1="18" x2="21" y2="18" /></svg>); const Menu = ({ size = 20 }) => (<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" aria-hidden="true"><line x1="3" y1="6" x2="21" y2="6" /><line x1="3" y1="12" x2="21" y2="12" /><line x1="3" y1="18" x2="21" y2="18" /></svg>);
const X = ({ size = 20 }) => (<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" /></svg>); const X = ({ size = 20 }) => (<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" /></svg>);
const RELEASE = '0.84.0-quality'; const RELEASE = '0.85.0-fastpaint';
const NAV = [ const NAV = [
{ id: 'home', label: 'Tavern' }, { id: 'home', label: 'Tavern' },