Files

6.0 KiB

name, description, license, metadata
name description license metadata
optimize-fonts Optimize a project's web fonts — subset the files, prefer variable fonts, add unicode-range and metric-override fallbacks to cut payload and remove layout shift (CLS). Use when the user asks to set up, optimize, subset or fix fonts, @font-face rules or font-family stacks. Fonts may come from Google Fonts or be local files. MIT
author version
Valentin Silyutin 1.0.0

Web Font Optimization Instructions

Replace full-coverage font files with subset files, prefer a variable font when one exists, wire unicode-range, and add a metric-override fallback so the switch from the fallback font to the web font on load does not shift layout (CLS).

Inputs

  • Family: a font family name such as Montserrat. If the user does not give one, detect it from the font files in the project.
  • Subsets: always cyrillic and latin. All projects in this workspace are Russian + Latin.
  • Paths: the font folder and the stylesheet that declares @font-face / font-family. Read them from the project's AGENTS.md; ask the user only if AGENTS.md does not say.

Workflow

1. Locate the fonts

Read AGENTS.md and find (a) the folder that holds the font files and (b) the file(s) where fonts are connected. Detect the family name, the weights the project ships, and which weights the styles actually use (grep for font-weight).

2. Try Google Fonts

Request the family's CSS with a desktop browser User-Agent (without it Google returns TTF instead of WOFF2):

curl -s -A "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" \
  "https://fonts.googleapis.com/css2?family=<Family>:wght@<min>..<max>&display=swap"
  • If the response declares font-weight: <min> <max>, it is a variable font — use it.
  • Otherwise request the individual weights the project uses (wght@400;500;700).
  • Take both the WOFF2 URLs and the unicode-range values for cyrillic and latin from the response. They are authoritative — do not hand-write them.
  • If the family is not on Google Fonts (a custom or local font), keep the project's own file(s) and subset them; derive unicode-range from the font cmap (see the reference section).

3. Place subset files

Download the cyrillic and latin WOFF2 files into the project's font folder, named <Family>-cyrillic.woff2 / <Family>-latin.woff2. For static fonts include the weight: <Family>-<weight>-latin.woff2. Ask the user before deleting the old files (WOFF fallbacks, full-coverage files) — they stop being referenced.

4. Rewrite the @font-face rules

Write one rule per subset, keeping the project's existing path style to the font folder:

@font-face {
    font-display: swap;
    font-family: <Family>;
    font-style: normal;
    font-weight: <min> <max>; /* single value for static fonts */
    src: url('./fonts/<Family>-<subset>.woff2') format('woff2');
    unicode-range: <from Google Fonts CSS or cmap>;
}

5. Add the metric-override fallback

Run the bundled script on the web font file; it prints ready @font-face blocks.

python3 scripts/font-metrics.py <path-to-webfont.woff2> --name <Family> --weights "<min> <max>"

Append the printed blocks, then put the fallback families into every font-family stack, right after the web font:

font-family: <Family>, '<Family> Fallback', '<Family> Fallback Android', system-ui;

Grep the whole project for <Family> in font-family declarations and update each one.

6. Drop unused weights

Grep font-weight and fix any weight the project does not ship (for example a lone 300 with no 300 file) to an existing weight.

Font metrics script

scripts/font-metrics.py reads the metrics from the font file and prints the fallback @font-face blocks. It needs Python 3 with fonttools and brotli installed (pip install fonttools brotli).

  • For a variable font it measures the instance at --weight (default 500, the dominant body weight).
  • Fallback metrics for Arial and Roboto are built in; it emits one block per fallback (Arial for Windows/macOS, Roboto for Android, where Arial is absent).

The numbers come from the font metrics:

size-adjust       = web.avg / fallback.avg        · 100%
ascent-override   = web.ascent / size-adjust      · 100%
descent-override  = web.descent / size-adjust     · 100%
line-gap-override = web.lineGap / size-adjust     · 100%

where avg is the mean advance width over a-z A-Z 0-9 space, normalised to unitsPerEm; ascent / descent / lineGap come from the font hhea table. size-adjust scales every metric, so the overrides are divided by it to land on the target line height.

Verification

  • Run the project's own linters and formatters on the changed files (see the project's AGENTS.md). If templates changed, rebuild and validate them the way the project does.
  • Build the project and confirm the new font files land in the build output and the new @font-face rules are present in the built CSS.

Reference

  • unicode-range lists the code points actually present in that subset file. The browser downloads a file only when a character in its range is used, so subsets absent from the page are never fetched. The default (when omitted) is U+0-10FFFF — the whole of Unicode — which defeats subsetting.
  • The Google latin subset is not just letters: it includes shared punctuation and symbols (dashes, quotes, €, ™, digits). That is why its range is long.
  • To derive a range from a font yourself: read the cmap table (fonttools), sort the code points and collapse consecutive runs into U+XXXX-YYYY.
  • size-adjust is supported by all current browsers; ascent-override / descent-override / line-gap-override are ignored by Safari (a known, harmless gap — it still applies size-adjust).
  • font-display: swap draws text immediately in the fallback font and swaps when the web font loads; the metric-override fallback is what keeps that swap from shifting layout.