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:
+16
-8
@@ -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
|
/// 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
|
/// 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`
|
/// of the `ASSET_VERSION` hash, so a new face mints a new URL. Neither face is
|
||||||
/// preloads the regular face, and the faces use `font-display: swap` behind
|
/// 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
|
/// metric-matched local fallbacks, so first paint is immediate and the swap
|
||||||
/// shifts nothing.
|
/// shifts nothing.
|
||||||
pub static APP_CSS: LazyLock<String> = LazyLock::new(|| {
|
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());
|
let expected = format!("/static/app.css?v={}", ASSET_VERSION.as_str());
|
||||||
assert!(html.contains(&expected), "{html}");
|
assert!(html.contains(&expected), "{html}");
|
||||||
assert!(!html.contains(&format!("/static/app.css?v={}", crate::VERSION)));
|
assert!(!html.contains(&format!("/static/app.css?v={}", crate::VERSION)));
|
||||||
// The regular face is preloaded so it starts downloading alongside the
|
// Nothing is preloaded. A preload of the 132 KB regular face shares
|
||||||
// stylesheet that declares it; the italic face is left to load on demand.
|
// bandwidth with the 12 KB render-blocking stylesheet, which is what
|
||||||
let preload = format!(
|
// first paint actually waits on: it pushed simulated FCP from ~1.0 s to
|
||||||
"<link rel=\"preload\" href=\"/static/Newsreader.woff2?v={}\" as=\"font\" type=\"font/woff2\" crossorigin>",
|
// ~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()
|
ASSET_VERSION.as_str()
|
||||||
);
|
);
|
||||||
assert!(html.contains(&preload), "{html}");
|
assert!(html.contains(&app_js), "{html}");
|
||||||
assert!(!html.contains("Newsreader-italic.woff2"), "{html}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -6,12 +6,11 @@
|
|||||||
<meta name="description" content="{{ page.description }}">
|
<meta name="description" content="{{ page.description }}">
|
||||||
<meta name="color-scheme" content="light dark">
|
<meta name="color-scheme" content="light dark">
|
||||||
<title>{{ page.title }} · The Daily EPUB</title>
|
<title>{{ page.title }} · The Daily EPUB</title>
|
||||||
{# Start the regular face downloading with the stylesheet rather than after
|
{# No font preload here on purpose. Preloading the 132 KB regular face made
|
||||||
it parses. `crossorigin` is required even same-origin: font fetches are
|
it share bandwidth with this 12 KB render-blocking sheet, so the sheet —
|
||||||
CORS-mode, so without it the preload would not match the @font-face
|
and therefore first paint — landed later: simulated FCP went from ~1.0 s
|
||||||
request and the file would be fetched twice. Only the regular face —
|
to ~1.8 s on Lighthouse mobile. The metric-matched fallbacks in
|
||||||
italic is rare enough that on-demand loading is the right trade. #}
|
tailwind.css make waiting for the real face cost nothing visible. #}
|
||||||
<link rel="preload" href="/static/Newsreader.woff2?v={{ page.asset_version }}" as="font" type="font/woff2" crossorigin>
|
|
||||||
<link rel="stylesheet" href="/static/app.css?v={{ page.asset_version }}">
|
<link rel="stylesheet" href="/static/app.css?v={{ page.asset_version }}">
|
||||||
{# Keep this synchronous script *after* the stylesheet: Gecko will not run a
|
{# Keep this synchronous script *after* the stylesheet: Gecko will not run a
|
||||||
parser-blocking script while a stylesheet is pending, so the parser stalls
|
parser-blocking script while a stylesheet is pending, so the parser stalls
|
||||||
@@ -58,6 +57,6 @@
|
|||||||
{% match page.flash %}{% when Some with (flash) %}<div class="mx-auto max-w-7xl px-4 sm:px-6"><div class="flash {{ flash.kind }}" role="status">{{ flash.text }}</div></div>{% when None %}{% endmatch %}
|
{% match page.flash %}{% when Some with (flash) %}<div class="mx-auto max-w-7xl px-4 sm:px-6"><div class="flash {{ flash.kind }}" role="status">{{ flash.text }}</div></div>{% when None %}{% endmatch %}
|
||||||
<main id="content" class="min-h-[68vh]">{% block content %}{% endblock %}</main>
|
<main id="content" class="min-h-[68vh]">{% block content %}{% endblock %}</main>
|
||||||
<footer class="mx-auto mt-16 flex max-w-7xl flex-wrap justify-between gap-2 border-t border-rule px-4 py-6 font-sans text-[0.72rem] uppercase tracking-[0.1em] text-muted sm:px-6"><span>The Daily EPUB</span><span>daily-epub {{ page.version }}</span></footer>
|
<footer class="mx-auto mt-16 flex max-w-7xl flex-wrap justify-between gap-2 border-t border-rule px-4 py-6 font-sans text-[0.72rem] uppercase tracking-[0.1em] text-muted sm:px-6"><span>The Daily EPUB</span><span>daily-epub {{ page.version }}</span></footer>
|
||||||
<script src="/static/app.js?v={{ page.asset_version }}"></script>
|
<script defer src="/static/app.js?v={{ page.asset_version }}"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user