Drop the font preload and defer app.js

Measured on a quiet machine behind a gzip proxy, three interleaved rounds
per variant: with the preload FCP 1.8 s, LCP 2.6 s, perf 0.95-0.96;
without it FCP 0.97 s, LCP 0.97 s, perf 1.00.

Lantern splits bandwidth between in-flight requests, so preloading the
132 KB regular face starved the 12 KB render-blocking stylesheet — and
the stylesheet, not the font, is what first paint waits on. The
metric-matched fallbacks already make the wait for the real face
invisible, so the preload bought nothing that first paint could see.

app.js gets `defer` for the same reason: Lighthouse counts the sync tag
among the render-blocking requests. It stays at the end of body, and it
is plain top-level DOM code with no readyState or DOMContentLoaded
dependence, so execution order is unchanged.

theme.js keeps its place: still synchronous, still after the stylesheet
link, so Gecko cannot paint unstyled.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MD4VWGq6mGcd8Bg67qyx9k
This commit is contained in:
2026-09-04 20:08:42 +00:00
co-authored by Claude Fable 5.1
parent 62803d73a6
commit 2388d7bd02
2 changed files with 22 additions and 15 deletions
+16 -8
View File
@@ -236,8 +236,9 @@ const NEWSREADER_ITALIC: &[u8] = include_bytes!("static/fonts/Newsreader-italic.
///
/// So the URLs stay URLs, carrying `?v=<ASSET_VERSION>` so the immutable
/// one-year `max-age` on `/static/*` is safe across deploys. The fonts are part
/// of the `ASSET_VERSION` hash, so a new face mints a new URL. `layout.html`
/// preloads the regular face, and the faces use `font-display: swap` behind
/// of the `ASSET_VERSION` hash, so a new face mints a new URL. Neither face is
/// preloaded — that only takes bandwidth from this sheet, which is what first
/// paint actually waits on. Instead both use `font-display: swap` behind
/// metric-matched local fallbacks, so first paint is immediate and the swap
/// shifts nothing.
pub static APP_CSS: LazyLock<String> = LazyLock::new(|| {
@@ -1123,14 +1124,21 @@ mod tests {
let expected = format!("/static/app.css?v={}", ASSET_VERSION.as_str());
assert!(html.contains(&expected), "{html}");
assert!(!html.contains(&format!("/static/app.css?v={}", crate::VERSION)));
// The regular face is preloaded so it starts downloading alongside the
// stylesheet that declares it; the italic face is left to load on demand.
let preload = format!(
"<link rel=\"preload\" href=\"/static/Newsreader.woff2?v={}\" as=\"font\" type=\"font/woff2\" crossorigin>",
// Nothing is preloaded. A preload of the 132 KB regular face shares
// bandwidth with the 12 KB render-blocking stylesheet, which is what
// first paint actually waits on: it pushed simulated FCP from ~1.0 s to
// ~1.8 s on Lighthouse mobile, for a face the metric-matched fallbacks
// already stand in for.
assert!(!html.contains("rel=\"preload\""), "{html}");
// The italic face is never referenced from the HTML either.
assert!(!html.contains("Newsreader-italic.woff2"), "{html}");
// `defer` keeps app.js out of Lighthouse's render-blocking list; it has
// no readyState or DOMContentLoaded dependence, so deferring is safe.
let app_js = format!(
"<script defer src=\"/static/app.js?v={}\"></script>",
ASSET_VERSION.as_str()
);
assert!(html.contains(&preload), "{html}");
assert!(!html.contains("Newsreader-italic.woff2"), "{html}");
assert!(html.contains(&app_js), "{html}");
}
#[test]