open-code-review/pages/index.html
chethanuk d6184df159
fix(pages): inline critical CSS to remove the initial white flash (#601) (#610)
* fix(pages): inline critical CSS to remove the initial white flash (#601)

Every rule in pages/src/styles/index.css — including
`body { background-color: #000000 }` — is imported from JS and injected at
runtime by style-loader, and the bundle is split into four deferred chunks.
So no stylesheet exists at all until the last chunk executes, and index.html
had no inline CSS: the browser painted its default white canvas for several
hundred milliseconds before the page turned black.

Inline a minimal critical block in the HtmlWebpackPlugin template, ahead of
the two render-blocking CDN stylesheet links, with the same #000000/#ffffff
as index.css so nothing shifts when the runtime CSS arrives. It applies to
`html` as well as `body`, since `html` is what the browser has resolved at
first paint.

Also add `<meta name="color-scheme" content="dark">`. The Google Fonts and
Font Awesome links are render-blocking, so first paint waits on them and the
inline style cannot help until they load; the meta is what gives the browser
a dark canvas in the meantime.

The boot indicator hangs off `#root:empty::after`, so React's first render
removes it with no JS teardown, and its animation is disabled under
prefers-reduced-motion.

One template feeds both HtmlWebpackPlugin instances, so dist/404.html (the
SPA deep-link fallback) gets the same treatment.

Tested: 7 table-driven vitest assertions over index.html, each verified to
fail when the block is removed. lint / typecheck / build / size-limit clean
(86.6 kB brotlied against the 150 kB gate), and both dist/index.html and
dist/404.html confirmed to carry the block ahead of the first stylesheet and
script tag.

* fix(pages): load CDN stylesheets async instead of declaring color-scheme

Review feedback: `<meta name="color-scheme" content="dark">` reaches further
than this fix needs. It switches UA-rendered form controls, scrollbars and
<dialog> backdrops to their dark variants for the whole site, permanently —
not just during the loading window it was added to cover.

Take the root cause instead. The meta was only needed because the Google
Fonts and Font Awesome stylesheets are render-blocking, so first paint waited
on two CDN round-trips and the inline critical block could not apply until
they resolved. Loading them with media="print" + an onload swap drops them out
of the critical path entirely, so the inline block paints immediately and the
meta has nothing left to do.

Nothing above the fold regresses: the font URL already carries display=swap,
so text renders in a fallback and swaps, and the only Font Awesome icons are
the three in WhySection, well below the initial viewport. <noscript> copies
keep both stylesheets applied when scripting is off, since the onload swap
cannot run there.

Also close the gap flagged on the inline colours: they are duplicated from
index.css out of necessity, and nothing enforced that they stay equal. The
test now parses the `body` rule out of index.css and compares, so editing one
without the other fails rather than shipping a visible shift.

Structural assertions now run against the markup with HTML comments stripped.
The comment explaining this block names the tags it discusses, and an
indexOf('<noscript>') was matching the prose rather than the element.

Tested: 12 vitest assertions, each of the three new ones mutation-checked in
isolation (make the links blocking / desync index.css / drop the noscript —
each fails exactly its own assertion). lint / typecheck / build / size clean,
86.6 kB brotlied against the 150 kB gate. Both dist/index.html and
dist/404.html verified to carry the inline block ahead of any stylesheet or
script, two async links, and the noscript fallback.
2026-08-05 15:13:47 +08:00

78 lines
3.9 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Open Code Review</title>
<meta name="description" content="The open source code review agent." />
<!-- Open Graph / social preview (iMessage, WeChat, Slack, Facebook, ...) -->
<meta property="og:type" content="website" />
<meta property="og:site_name" content="Open Code Review" />
<meta property="og:url" content="https://open-codereview.ai/" />
<meta property="og:title" content="Open Code Review" />
<meta property="og:description" content="The open source code review agent." />
<meta property="og:image" content="https://open-codereview.ai/og-image.png" />
<!-- Twitter / X -->
<meta name="twitter:card" content="summary" />
<meta name="twitter:title" content="Open Code Review" />
<meta name="twitter:description" content="The open source code review agent." />
<meta name="twitter:image" content="https://open-codereview.ai/og-image.png" />
<!-- Critical paint state, inlined because every rule in src/styles/index.css
ships inside the deferred JS chunks (style-loader, no MiniCssExtractPlugin).
Until the last chunk executes there is no stylesheet at all, so the browser
paints its default white canvas. Colours must stay in sync with the `body`
rule in src/styles/index.css so nothing shifts when the runtime CSS lands;
src/index.html.test.ts asserts they still match. Keep this block ahead of
the links below — that ordering is what makes it critical CSS. -->
<style>
html, body { margin: 0; background-color: #000000; color: #ffffff; }
/* Boot indicator. Present only while #root has no children, so React mounting
removes it with no JS teardown of our own. */
#root:empty::after {
content: "";
position: fixed;
top: 50%;
left: 50%;
width: 28px;
height: 28px;
margin: -14px 0 0 -14px;
border: 2px solid rgba(255, 255, 255, 0.15);
border-top-color: rgba(255, 255, 255, 0.6);
border-radius: 50%;
animation: ocr-boot-spin 0.8s linear infinite;
}
@keyframes ocr-boot-spin { to { transform: rotate(360deg); } }
/* Still shows the ring, just without the motion. */
@media (prefers-reduced-motion: reduce) {
#root:empty::after { animation: none; }
}
</style>
<link rel="icon" type="image/svg+xml" href="<%= require('./logo.svg') %>" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<!-- Loaded without blocking first paint: `media="print"` does not match the
screen, so the browser fetches these without holding up rendering, and the
onload handler switches them on once they arrive. Otherwise the two CDN
round-trips gate first paint and the inline block above cannot apply until
they finish. Text swaps in via the font URL's own display=swap, and the
Font Awesome icons only appear well below the fold (WhySection).
The <noscript> copies keep both stylesheets render-blocking-but-applied
when scripting is off, since the onload swap cannot run there. -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet" media="print" onload="this.media='all'" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" media="print" onload="this.media='all'" />
<noscript>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" />
</noscript>
</head>
<body>
<div id="root"></div>
</body>
</html>