ADR-058: Font Preload Budget Gate
Status
Section titled “Status”Accepted
Context
Section titled “Context”An adopter audit surfaced a subtle regression class: over-preloading fonts.
<link rel="preload" as="font"> fetches a font at highest priority, competing
for bandwidth with the LCP resource. One or two preloads warm the critical
fonts; beyond that, each additional preload delays the very paint it was meant
to speed up. The failure is quiet — nothing errors, the page just gets slower,
and Lighthouse’s category score absorbs the loss without naming the cause.
This starter uses the Astro Fonts API (ADR-053)
with two families (Geist display, Inter text), and currently preloads exactly
two variable woff2 files — one per family — on every page. That is the
correct, deliberate number. Nothing stops a future change (adding a weight, a
third family, a second script subset) from quietly pushing the preload count up
and eroding LCP. The JS, CSS, image, and Lighthouse budgets are all gated;
font preloads are not.
Decision Drivers
Section titled “Decision Drivers”- Protect LCP: preloads are a scarce, high-priority resource; more is not better past the critical fonts.
- Halt-on-violation parity (ADR-039): make the implicit “two preloads” rule explicit and enforced.
- Fit the existing gate architecture: reuse the build-output-scan pattern the JS bundle-size and image gates already use.
- Configurable: adopters with different font strategies can raise the cap.
Considered Options
Section titled “Considered Options”Option 1: Build-time scan of the emitted HTML
Section titled “Option 1: Build-time scan of the emitted HTML”Description: A script counts rel="preload" … as="font" links per page in
dist/**/*.html after build and fails if any page exceeds the cap (default 2,
MAX_FONT_PRELOADS override).
Pros:
- Measures what actually ships, regardless of how the preloads were produced
(Astro Fonts API, a manual
<link>, a future integration). - Mirrors the existing JS-bundle and image gates (post-build CI step, pure testable core). No new concepts.
Cons:
- Needs a build before it can run (already true for the sibling gates).
Option 2: Lighthouse CI budget assertion
Section titled “Option 2: Lighthouse CI budget assertion”Description: Assert a resource-count budget on fonts in lighthouserc.
Pros:
- No new script.
Cons:
- LHCI resource-summary budgets count all font requests, not preloads specifically — it can’t distinguish a preload from a normal font load, which is exactly the signal we care about. Wrong instrument.
Option 3: Biome/lint rule on source
Section titled “Option 3: Biome/lint rule on source”Description: Lint the source for preload <link>s.
Pros:
- Runs in
quality:ciwith no build.
Cons:
- The preloads are generated by the Astro Fonts API, not written in source. A source lint is blind to them. Wrong layer.
Decision
Section titled “Decision”We will implement Option 1: scripts/src/check-font-preloads.ts counts
preloaded font files per built page and fails CI when any page exceeds the cap.
- Default cap: 2 preloaded font files per page — the current, deliberate
count (one per family). Override with
MAX_FONT_PRELOADS=<n>. - Placement: a post-build CI step alongside the JS bundle-size and image
gates. New
fonts:gatescript. - Scope:
<link rel="preload" … as="font">occurrences perdistHTML page. It counts preload links, which is the LCP-relevant signal.
Implementation Details
Section titled “Implementation Details”// Pure, unit-tested core — the CLI wires it to the built HTML.export function findFontPreloadViolations( pages: FontPreloadCount[], maxPreloads: number,): FontPreloadCount[] { return pages .filter((page) => page.count > maxPreloads) .sort((a, b) => b.count - a.count);}Consequences
Section titled “Consequences”Positive
Section titled “Positive”- Over-preloading fonts becomes a hard CI failure with a per-page report, not a silent LCP tax.
- The deliberate “two preloads” decision is now enforced, not just documented.
Negative
Section titled “Negative”- Counts preload links, not their byte weight — two enormous preloaded fonts
would pass. The per-image gate has no font analogue; font weight is bounded
in practice by the
woff2variable-subset strategy (ADR-053), not by this gate.
Neutral
Section titled “Neutral”- Adopters who legitimately need more preloads raise
MAX_FONT_PRELOADS.
Validation
Section titled “Validation”- Metric 1: a fixture page with 3 font preloads fails at cap 2; a page with 2 passes. Covered by unit + fixture tests.
- Metric 2: the current demo passes — every one of its 84 built pages preloads exactly two fonts.
References
Section titled “References”- ADR-053: Fonts via the Astro Fonts API
- ADR-026: Font Strategy
- ADR-039: Halt-on-Violation Enforcement
- ADR-057: Per-Image Size Budget Gate — sibling build-output gate
The cap counts links, deliberately. The point is to stop preload proliferation
competing with LCP, not to police individual font size (the variable-subset
woff2 strategy already keeps each face small).
Enforcement
Section titled “Enforcement”- Testable consequences:
- TC-1: no emitted page exceeds the font preload cap.
- Checks:
- TC-1 →
fonts:gatein CI (status: block, pre-existing gate)
- TC-1 →
- Not machine-checkable: which fonts deserve the scarce preload slots.
- Graduation log: (empty at creation; entries added when a check changes status)
Date: 2026-07-02
Participants: Engineering
Outcome: Accepted