From 2388d7bd024d76c6e1f9bfb11a331d6a8ea643fe Mon Sep 17 00:00:00 2001 From: Tyler Hallada Date: Fri, 4 Sep 2026 20:08:42 +0000 Subject: [PATCH] Drop the font preload and defer app.js MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01MD4VWGq6mGcd8Bg67qyx9k --- src/web/mod.rs | 24 ++++++++++++++++-------- src/web/templates/layout.html | 13 ++++++------- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/src/web/mod.rs b/src/web/mod.rs index 4934dcd..bb7337d 100644 --- a/src/web/mod.rs +++ b/src/web/mod.rs @@ -236,8 +236,9 @@ const NEWSREADER_ITALIC: &[u8] = include_bytes!("static/fonts/Newsreader-italic. /// /// So the URLs stay URLs, carrying `?v=` 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 = 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!( - "", + // 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!( + "", ASSET_VERSION.as_str() ); - assert!(html.contains(&preload), "{html}"); - assert!(!html.contains("Newsreader-italic.woff2"), "{html}"); + assert!(html.contains(&app_js), "{html}"); } #[test] diff --git a/src/web/templates/layout.html b/src/web/templates/layout.html index 2b19a53..3ccfdd2 100644 --- a/src/web/templates/layout.html +++ b/src/web/templates/layout.html @@ -6,12 +6,11 @@ {{ page.title }} · The Daily EPUB - {# Start the regular face downloading with the stylesheet rather than after - it parses. `crossorigin` is required even same-origin: font fetches are - CORS-mode, so without it the preload would not match the @font-face - request and the file would be fetched twice. Only the regular face — - italic is rare enough that on-demand loading is the right trade. #} - + {# No font preload here on purpose. Preloading the 132 KB regular face made + it share bandwidth with this 12 KB render-blocking sheet, so the sheet — + and therefore first paint — landed later: simulated FCP went from ~1.0 s + to ~1.8 s on Lighthouse mobile. The metric-matched fallbacks in + tailwind.css make waiting for the real face cost nothing visible. #} {# Keep this synchronous script *after* the stylesheet: Gecko will not run a parser-blocking script while a stylesheet is pending, so the parser stalls @@ -58,6 +57,6 @@ {% match page.flash %}{% when Some with (flash) %}
{{ flash.text }}
{% when None %}{% endmatch %}
{% block content %}{% endblock %}
The Daily EPUBdaily-epub {{ page.version }}
- +