5 min read
web-font-optimization
Generated source view for the actual executable
engineering/web-font-optimizationskill. The durable routing article is Media Generation Skills. Source: skills/engineering/web-font-optimization/SKILL.md
Runtime Source
| Field | Value |
|---|---|
| Category | engineering |
| Origin | personal |
| Slug | web-font-optimization |
| Source slug | web-font-optimization |
| Family | Media Generation Skills |
| Source | skills/engineering/web-font-optimization/SKILL.md |
Bundled Resources
No bundled resource files.
Description
Make web fonts load faster without changing rendering: measure usage, subset into a preloaded core and a unicode-range file with a committed script, prove it with pixel diffs. Use when fonts are heavy or dominate LCP, or on "optimize the fonts".
Skill Source
---
name: web-font-optimization
description: >-
Make web fonts load faster without changing rendering: measure usage, subset into a preloaded
core and a unicode-range file with a committed script, prove it with pixel diffs. Use when fonts
are heavy or dominate LCP, or on "optimize the fonts".
origin: personal
source_slug: web-font-optimization
---
# Web font optimization
Kevin's brief (2026-10-30), kept as the contract:
> Make this site's web fonts load faster without changing how anything looks. Shrink font bytes as far as possible, keep rendering identical, and prove it with before/after pixel diffs. Treat any visual change as a regression unless I approve it.
## Ground rules
- Any visual change is a regression unless Kevin approves it. When an optimization would change rendering, report what it saves and what it changes, and keep the site looking the same by default.
- Keep the current loading behaviour: do not touch `font-display` or the fallback fonts.
- Do not add font processing to the build. Write a script Kevin runs by hand, commit its output, and note in the script header when it needs rerunning (new characters in content, a new weight or axis value, a new OpenType feature, a new font version).
## 1. Measure the current state
For each font file record: bytes, glyph count, variable axes and their ranges, OpenType features (GSUB and GPOS feature tags), and hinting (presence of `fpgm`, `prep`, `cvt `, `gasp`, `hdmx` and TrueType instructions). Then record font bytes per page and what each page preloads. Keep this as the "before" table.
## 2. Find what the site actually uses
Check stylesheets and inline component styles, not just CSS files.
- **Characters.** Collect every character that appears in the content: page text, generated pages, metadata the browser renders, form placeholders, CSS `content:` strings, and text set by scripts. Include CSS side effects such as `hyphens: auto` inserting U+2010 and `text-transform` changing case.
- **Axis values and weights.** Every weight and axis value each font needs, including one-off overrides and bold inside italics.
- **OpenType features.** Every feature turned on, by `font-feature-settings`, `font-variant-*`, or defaults the shaper applies (`kern`, `liga`, `calt`, `locl`, `mark`, `mkmk`, `ccmp`).
## 3. Cut each font down
- Keep the used characters plus Latin-1 Supplement and Latin Extended-A, so new content rarely needs a rerun.
- Leave CJK and other large scripts to system fonts if that is how the page renders today.
- Limit variable axes to the ranges in use. Fix an axis at one value only if nothing overrides it anywhere.
- Drop unused OpenType features and drop hinting.
- Split each font into two files:
- a small **core** with ASCII and the punctuation most pages use (curly quotes, dashes, ellipsis), which pages preload;
- an **extended** file behind `unicode-range` that downloads only when a page uses one of its characters.
- Each file's `unicode-range` lists only characters the file actually contains. Generate it from the subset's cmap, never by hand.
## 4. Pitfalls
- A generated fallback face placed between the core and extended `@font-face` rules draws the extended characters before the real font does. Keep the core and extended faces under the same family name with no fallback face between them.
- Fonts with a Reserved Font Name under the SIL OFL must be renamed internally once subset (name table IDs 1, 4, 6, 16 and the PostScript name). Check the license's RFN line; the author of the font can waive it for their own site, and that waiver is written down.
- Inspect the generated files: open each in fontTools, confirm glyph count, cmap coverage, axis ranges (`fvar`, `avar`, `STAT` consistency after instancing) and that `name` records are intact.
## 5. Prove it
- Build a baseline from the same content with the original fonts. Pixel-diff before and after at the same viewport and device scale on pages with code, italics, accented characters, components with their own font settings, and CJK if present.
- Explain every visible difference as a content change, a regression to fix, or harmless sub-pixel rounding. A non-zero diff in glyph shapes is a regression.
- Re-measure bytes per page and confirm the extended files load only on pages that use their characters.
## 6. Report
- Bytes before and after, per font and per page.
- The pixel-diff results per page.
- Anywhere you deviated from the brief and what it cost.
- How to rerun the script and when it needs rerunning.
- Possible next step: embed the core font in the global stylesheet as a data URL and precache the extended files after idle.
## Tools
`pyftsubset` and `fontTools.varLib.instancer` (Python `fonttools` with `brotli` for WOFF2) do the cutting. A Playwright script that loads each page twice (baseline fonts, optimized fonts) and compares screenshots with `pixelmatch` or ImageMagick `compare -metric AE` does the proving.
Timeline
- 2026-11-02 | Generated a browseable source page from the actual executable skill file. Source: skills/engineering/web-font-optimization/SKILL.md