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 }}
- +