2026-10-31 · 22 min · fonts · web-performance · agents · claude-code · subsetting · woff2
Why read this
Notabletop 60%Runs the prompt on three OFL fonts with pixel diffs and throttled Chromium: Inter 350,516 B to 30,664 B; subsetting is pixel-identical, the weight trim is not.
- Original analysis
- Runs on a laptop CPU
- Widely used
Developer tools & infraOFL-1.1Practitioner guide
How this was scored
- Is it new?
- 1 of 3: An incremental tweak
- Can I trust it?
- 2 of 3: Measures key facts from files, code or configs
- Can I run it?
- 2 of 3: Open code or weights with real limits
- Will I understand it?
- 2 of 3: Mechanism from first principles with figures
- Can I act on it?
- 2 of 3: A concrete recipe, numbers or comparison
- Will it last?
- 2 of 3: A reference for a year or more
- Does it affect many?
- 2 of 3: A widely used model, tool or lab release
- Only here?
- 2 of 3: A teardown or measurement few others did
Score 65 of 100, ranked 256 of 666 rated articles. Each question is answered 0–3 by hand, and a 3 is rare. How articles are scored
@shuding posted: "Tell your agent to optimize your site's web fonts. I'm seeing a huge difference on my blog." Then he pasted the whole prompt, about 500 words, which is the interesting part. It is not a trick. It is a specification of what a careful person would do, written down so an agent does not skip the boring steps and does not skip the checking.
I wanted to know which of its claims survive contact with real fonts. So I downloaded three open-licence variable fonts, subset them to the characters this site actually prints, and measured bytes, pixels and load behaviour. I did not look at his blog or its numbers; this article is about the technique and the prompt, not his result. Every figure is labelled measured (I produced it, the commands are below), reported (someone else's) or reasoned (my arithmetic on the other two).
The prompt
This is the full text as the post gives it (fetched through the fxtwitter mirror, quoted verbatim):
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. If an optimization would change rendering, tell me what it saves and what it changes, and keep the site looking the same by default. Keep the current loading behavior: don't touch font-display or fallback fonts. Don't add font processing to the build; write a script I run by hand, commit its output, and note when it needs rerunning. First measure the current state: each font file's size, glyph count, variable axes, OpenType features and hinting, plus font bytes per page and what each page preloads. Then find out what the site actually uses, checking inline component styles as well as stylesheets: which characters appear in the content, which axis values and weights each font needs (including one-off overrides and bold inside italics), which OpenType features are turned on, and CSS side effects like
hyphens: autousing the U+2010 hyphen. Cut each font down to that. Keep the used characters plus Latin-1 and Latin Extended-A, and leave CJK to system fonts if that's how it renders today. Limit variable axes to the ranges in use, and only fix an axis at one value if nothing overrides it. Drop unused OpenType features and hinting. Split each font into two files. A small core with ASCII and the punctuation most pages use (curly quotes, dashes, ellipsis) gets preloaded. An extended file behindunicode-rangeonly downloads when a page needs one of its characters. Each file'sunicode-rangeshould list only characters it actually contains. Watch out for a few things. A generated fallback face between the core and extended files will draw the extended characters before the real font does. Fonts with a Reserved Font Name under the OFL must be renamed internally once subset. Check the generated files, since font tools have quirks. Before calling it done, build a baseline from the same content and pixel-diff before and after on pages with code, italics, accented characters, components with their own font settings, and CJK. Explain every visible difference: content change, regression to fix, or harmless sub-pixel rounding. Re-measure bytes per page and confirm the extended files only load where needed. When you're done, tell me the bytes before and after per font and per page, the pixel-diff results, anywhere you deviated from what I asked and what it cost, and how to rerun the script. Later, it may be worth embedding the core font in the global stylesheet and precaching the extended files after idle.
Read it as a contract: a goal (faster, identical), an oracle (pixel diffs; any visual change is a regression), constraints (leave font-display alone, no build step), a method (measure, discover usage, cut, split) and a report format. The method is ordinary. The oracle and the report format are what make it a prompt for an agent, and they are where I spent most of my time.
What I ran
All of it on a 4 vCPU Linux VM, CPU only, no GPU. Versions: fontTools 4.68.0 with brotli, Python 3.13, Chromium 141.0.7390.37 (headless, driven by Playwright 1.64.1), Pillow and scikit-image for the diffs. The fonts came from the google/fonts repository through a CDN mirror and are all SIL Open Font License 1.1:
| font | role | file | glyphs | axes | WOFF2, untouched |
|---|---|---|---|---|---|
| Inter | sans, body | 876,576 B TTF | 2,933 | wght 100-900, opsz 14-32 | 350,516 B |
| Source Serif 4 | serif, long reads | 1,209,508 B TTF | 1,463 | wght 200-900, opsz 8-60 | 428,076 B |
| JetBrains Mono | code | 187,208 B TTF | 1,179 | wght 100-800 | 71,548 B |
The "site" is this one. I read all 669 MDX files under content/ and split the text in two: prose (code fences and
inline code removed) for the sans and serif, and code for the mono. That gave 15,028,213 characters of prose using 730
distinct code points, and 985,472 characters of code using 487. Measured. I do not commit the font files; the snippets below reproduce the outputs.
A first measurement the prompt asks for surprised me: the three files carry no glyph hinting to drop. I counted glyphs with TrueType instructions and found zero in all three; each has only a tiny prep and gasp pair. Dropping hinting is worth a few hundred bytes (Inter 90,212 B hinted against 90,052 B unhinted; the mono came out 60 B larger). On older TTFs it would matter. On current Google variable fonts it is a rounding error, and the prompt's own measurement step should tell the agent to skip it.
Where the bytes go
A variable font is mostly not outlines. Here are the sfnt tables of each file, uncompressed, at four stages. Pick a font.
In Inter, gvar (the per-glyph deltas that make the weight and optical-size axes work) is 454,714 B of 876,213, more
than half. GPOS, the kerning and mark positioning, is another 153,772 B. The outlines themselves, glyf, are 137,900 B.
Source Serif 4 is the same story at a larger scale: 598,758 B of gvar and 422,184 B of GPOS out of 1,209,127.
Measured, from the table directory. Two consequences. Trimming the characters shrinks gvar and GPOS along with the
outlines, because both are per-glyph and per-pair. And trimming an axis attacks the single biggest table directly, which
is why the axis step in the ladder below is worth more than it looks.
The ladder
The prompt's steps, in order, applied to each font. Every rung is a file I built, and every size is the exact byte count. The charset at the subset rungs is the characters the site uses plus Latin-1 and Latin Extended-A, intersected with what the font has, as the prompt says.
WOFF2 alone is most of the cheap win: 876,576 B to 350,516 B for Inter, with no change in the tables. If a site still ships TTF, that is step one.
The feature step is the surprise. Keeping every OpenType feature while subsetting leaves Inter at 121,672 B. Letting fontTools apply its default feature list takes it to 90,052 B, and Source Serif 4 from 256,216 B to 165,352 B: a third of the file, gone, for features the page may never ask for. It is also the step that can change pixels, which I come to.
Splitting is not free. Core plus extended is 75,144 B for Inter against 63,956 B as one file. If every page needed both, you would pay 11,188 B for the privilege. It only wins if most pages need only the core, which is a claim about your content, and the next-but-one section tests it on mine.
For the mono, the numbers are smaller because there is less to cut: 71,548 B of WOFF2 to 34,692 B with the weight axis limited, 23,516 B for its core.
The commands behind it are about this long (my scripts were about a hundred lines across four files, most of it bookkeeping):
from fontTools import subset
from fontTools.varLib import instancer
from fontTools.ttLib import TTFont
import io
def trim_weight(path, lo, hi):
f = instancer.instantiateVariableFont(TTFont(path), {"wght": (lo, hi)})
buf = io.BytesIO(); f.save(buf); buf.seek(0)
return TTFont(buf) # round-trip: subsetting the instancer's output in place raises KeyError '.notdef'
def cut(font, codepoints, features=("*",)):
o = subset.Options(); o.layout_features = list(features); o.hinting = False
o.notdef_outline = True; o.glyph_names = False
s = subset.Subsetter(o); s.populate(unicodes=sorted(codepoints)); s.subset(font)
font.flavor = "woff2"; return fontThat KeyError is the "font tools have quirks" the prompt warns about, met on my first run: instantiateVariableFont with a
partial range leaves lazily-loaded gvar data that the subsetter then trips on. Saving to memory and reloading fixes it.
I only noticed because the script failed. A quirk that does not fail loudly is the kind the pixel diff exists to catch.
"Without changing how anything looks": the oracle, run
I rendered 73 text lines in headless Chromium, once with the original WOFF2 (full range, font-weight: 100 900) and once
with the candidate, at device scale 1, and compared the element screenshots pixel for pixel (grey-scale, absolute
difference, plus SSIM). The lines cover each font at weights 400, 500, 600 and 700 at 12, 16 and 24 px; accented text
from Latin-1 and Latin Extended-A; ligature and arrow strings; synthetic italic (these fonts have no italic file, so the
browser slants them); weights 300 and 800 outside the range; and six font-feature-settings values. Measured.
Rung by rung
| change | result |
|---|---|
| WOFF2 only | not diffed on its own: the WOFF2 is the baseline for every row, and it is a lossless wrapper around the same tables |
| subset, all features, hinting dropped | 71 of 73 identical. The two that differ are explained below |
| subset, fontTools default features | the same (hinting kept or dropped makes no pixel difference), until the CSS turns a dropped feature on |
| plus wght limited to 400-700 | weight 400 identical; 500, 600 and 700 are not |
| wght pinned to 400 (opsz free) | weights above 400 become the browser's synthetic bold: 2 to 20 percent of pixels differ |
The two differences in the subsetting-only rung are exactly the failure the prompt is built around, and neither is a
tooling error. One is a line I added on purpose: characters that Inter has but the subset left out, which fall back to a
system font and differ in 3.4 percent of pixels. The other I did not plan. My mono subset was built from the characters
inside code on the site, and my arrow-and-Greek test line contained τ and κ, which appear in this site's prose but not
in its code. The original mono face drew them; the subset face does not. That line differs in 0.59 percent of pixels, with
a maximum grey-level change of 237. Same bug, one level up: the "characters used" set is defined per font, by whichever
text the CSS sends to that font. Get the routing wrong (a <code> inside a heading, a table in the mono) and the set is
wrong.
The default-feature rung
The subsetter's default list keeps what shaping needs (kern, calt, ccmp, locl, mark, mkmk, fractions) and drops
the rest. In Inter that removes tnum, zero, case, cv01 to cv13 and ss01 to ss08, among others. Dropping them
is invisible until a stylesheet asks. Setting font-feature-settings on the subset of Inter, against the original:
"tnum": 4.7 percent of the line's pixels differ, maximum change 255"ss01": 3.4 percent;"cv11": 1.5 percent;"case": 1.1 percent;"zero": 0.1 percent- Source Serif 4:
"case"3.8 percent,"zero"0.18 percent; JetBrains Mono:"zero"0.34 percent
Those are the lines where the page would render differently. Keeping all features (the 121,672 B Inter file) made every
one of them pixel-identical. So the prompt's "which OpenType features are turned on" is the right question, and its answer
is not in the stylesheet alone: font-variant-numeric: tabular-nums, font-variant-caps, a Tailwind tabular-nums
class or a component's inline style all become tnum or smcp features at layout time. An agent that greps only *.css
for font-feature-settings will conclude nothing is used. The prompt does tell it to check inline component styles, which
is the part of the prompt I would underline.
The weight trim is not free
This was the result I did not expect. Limiting wght to 400-700 saves 26,000 B on Inter (90,052 B to 63,956 B) and the
prompt allows it ("limit variable axes to the ranges in use"), under the rule that rendering stays identical. It does not,
quite.
| font | weight 500-700 lines, share of pixels that differ | max grey-level change | SSIM, worst line |
|---|---|---|---|
| Inter | 0.28 to 3.06 percent | 81 | 0.99637 |
| Source Serif 4 | 0.92 to 6.69 percent | 157 | 0.99200 |
| JetBrains Mono | 0.21 to 1.35 percent | 64 | 0.99966 |
At weight 400, the default, there is no difference at all. Between 400 and 700 there is a small one. I took the
instanced fonts apart to find out why. I pinned both the full-range subset and the 400-700 subset to wght 500, 600 and 700
and compared coordinates. In Inter, the largest outline point moves by 0.375 font units at 500, 0.747 at 600 and 1.119 at
700 (units per em: 2,048); in Source Serif 4, by 0.27, 0.54 and 1.15 (units per em: 1,000). Advance widths differ by one
unit in 81 of 548 Inter glyphs at 600 and in none at 700. Measured. This is instancer's documented behaviour for a
limited range: the new axis maximum is exact, and the in-between positions are recomputed from re-normalised, re-quantised
deltas. At 24 px, 1.1 font units is about 0.013 px. Almost everything that differs is anti-aliasing at glyph edges, and
the maximum change of 157 grey levels is a glyph that landed on the other side of a pixel boundary. It is the "harmless
sub-pixel rounding" the prompt allows for, and it is also a change the prompt says to treat as a regression unless approved.
The honest report is a deviation line: "limiting wght to 400-700 saves 26,096 B on Inter and moves edge pixels at 500 to
700; approve or revert."
Pinning the axis to one value is far worse. I declared the 400-only file as font-weight: 400, and every bold on the page
became a browser-synthesised bold. The prompt's own condition, "only fix an axis at one value if nothing overrides it",
is not a nicety, and the agent has to find the overrides to meet it. The out-of-range check shows the cost of
missing one: a font-weight: 300 or 800 against the 400-700 file clamps to 400 or 700, with 10 to 16 percent of pixels
changed on the sans and serif (SSIM 0.73 to 0.82).
Whether the core file is the right core
The prompt's core is ASCII plus curly quotes, dashes and an ellipsis, "the punctuation most pages use". That is a
claim about content, so I tested it on mine. For each of the 624 articles, I took the prose, removed code, and asked: does
this page use any character that Inter has and that is not in the prompt's core? 519 of them do, 83 percent. The
usual suspects, by number of articles: × in 355, → in 202, − (U+2212) in 166, · in 157, ± in 64, ² in 62,
≈ in 55. Adding eight of those to the core still leaves 373 articles (60 percent) fetching the extended file, because the
tail is long: Greek letters, subscripts, daggers.
It is a technical site; a personal blog in plain prose would pass the prompt's test easily. The lesson is not that the prompt
is wrong. It is that "the punctuation most pages use" has to be a measured list, not a recalled one, and that the right
core is whatever minimises expected bytes over your pages. For this site that argues for either a larger core or two
extended files, split by use. The prompt is sound where it says the extended file's unicode-range "should list only
characters it actually contains", which I followed:
@font-face { font-family: "Inter X"; src: url(Inter-core.woff2) format("woff2"); font-weight: 400 700;
unicode-range: U+20-7E, U+A0, U+2013-2014, U+2018-2019, U+201C-201D, U+2026; }
@font-face { font-family: "Inter X"; src: url(Inter-ext.woff2) format("woff2"); font-weight: 400 700;
unicode-range: U+A1-AC, U+AE-148, U+14A-17F, U+251, U+259, /* ... generated from the file's cmap */; }I loaded a page of plain ASCII and a page containing Łódź ńąęśćż Çağrı with a request log. Measured: the first
requests Inter-core.woff2 and nothing else; the second requests the core and the extended file. That is the prompt's
"confirm the extended files only load where needed", and it held. The range strings come from cmap, which is also the
easiest way to produce a list that is honest.
Load behaviour, and the fallback trap
Smaller fonts only matter if the page waits less. I built a small page (a 34 px bold heading, three paragraphs at 18 px, a
box under them), served it from a local HTTP server, and loaded it in headless Chromium with the CDP network emulator at two
profiles, 1.6 Mbps with 150 ms latency and 9 Mbps with 40 ms, caching off, seven loads per variant. font-display: swap,
fallback Liberation Sans, the font preloaded in both cases. I recorded layout-shift entries (CLS), First Contentful Paint
and the time document.fonts.ready resolved. Measured, on a local server, so there is no real-world jitter: the seven
runs agree to within a few percent.
On the slow profile, the original 350,516 B file was ready at a median of 2,107 ms and the 30,664 B core at 487 ms. On the
fast profile, 444 ms and 141 ms. CLS for the unadjusted fallback was 0.027 in all of those but one: on the fast profile
the core arrived (141 ms) before First Contentful Paint (156 ms), so, I infer from the zero, the browser never painted the fallback and there was
nothing to shift. That is the best possible outcome for a font, and it is a consequence of the file size, not of any
font-display setting.
Two honest caveats. First, LCP did not move: the text's Largest Contentful Paint time equals its First Contentful Paint in every run (about 290 to 296 ms on the slow profile for both sizes), because the metric records the first paint of the text and not the later swap. A smaller font does not show up in LCP on a text-led page; it shows up in CLS and in how long the page looks unfinished. Second, I ran one page on one machine. I would not quote 0.027 as a typical CLS, only as the amount the fallback in this test moved the box below it.
size-adjust
The prompt says "don't touch fallback fonts", and it is easy to see why, since the correction is the one thing here that
changes what the user sees before the font arrives. I measured it anyway, as a deviation the agent could offer. The three
paragraphs at 100 px set in Inter total 23,008.8 px wide; in Liberation Sans, 23,600.25 px. Ratio 0.9749, so
size-adjust: 97.49%. From Inter's hhea table (ascent 1,984 and descent 494 of 2,048 units, no line gap), dividing by the
same factor gives ascent-override: 99.37% and descent-override: 24.74%. With that fallback declared as its own family
placed after Inter in the stack, CLS went to 0.000 on both profiles, original or optimised font. The text wraps to
3 + 3 + 2 lines in both.
I also tried to reproduce the prompt's warning about "a generated fallback face between the core and extended files". I
declared a face with the same family name, src: local("Liberation Sans"), no unicode-range, once after the core and
extended faces and once before them, and loaded the Łódź page. In Chromium 141 both faces were still requested and the
final render was pixel-identical to the separate-family version (0 differing pixels). I did not reproduce the problem
as described. What I did not test is the frames in between, nor Safari or Firefox, and a fallback face with a url()
source rather than local() might behave differently. The prompt's advice (keep the fallback in a separate family) costs
nothing and matches the CSS spec's matching rules, so I would follow it. I cannot show the failure it guards against.
The Reserved Font Name catch
Source Serif 4's copyright line says "with Reserved Font Name 'Source'". The OFL lets you subset and redistribute, but a modified version may not use a reserved name, so the subset must be renamed. The subsetter, by default, keeps the name table. I counted the English Windows name records of my subset that still contain "Source": the family name (ID 1), the unique ID (3), the full name (4), the PostScript name (6), the typographic name (25), and the four variable-font instance PostScript names (IDs 349 to 352), plus the trademark notice (ID 7), which must stay. Renaming only IDs 1, 4 and 6, which is what a quick script does, leaves six records behind. Measured, and it is the clearest case in the prompt where "check the generated files" means something specific: grep the name table, do not trust the rename. Inter and JetBrains Mono carry no Reserved Font Name in their copyright strings.
What an agent can verify, and what it cannot
Putting it together, a fair reading of the prompt:
It can verify, mechanically: file sizes; glyph counts; axes, features and table sizes from fontTools; whether a
character in the content exists in the font (I found 484 of Inter's 730 used code points missing from it: 362 CJK
characters plus Devanagari, Kannada and Arabic, which is the "leave CJK to system fonts" case, confirmed by the font's
own cmap); that a unicode-range string matches a file's cmap; the request log for a given page; a pixel diff between
two renders of the same text.
It can verify only if it asks the right question: whether a feature is "used" (it has to resolve tabular-nums and
font-variant-caps to tnum and smcp, and see features that component libraries toggle at runtime); whether an axis value
is "overridden" (every font-weight, every Tailwind font-* class, every <b>, <strong>, <th> and heading default,
and the default bold the browser synthesises inside italics); which text goes to which font.
It cannot verify at all: content that does not exist yet. A subset built from today's MDX renders a character from tomorrow's post, a comment, a user's name or a CMS field in the fallback font, with a different width, and no pixel diff of today's pages will show it. The prompt's "note when it needs rerunning" is the right half-answer: rerun when content changes. A better one is a CI check that fails when a content file contains a character no shipped font covers, which is a ten-line script and the only protection against the user-generated case. The prompt says to keep Latin-1 and Latin Extended-A for exactly this reason, and it is the right cushion for names with accents. It is no protection for a Vietnamese or Turkish name stacked with diacritics, or an emoji.
The pixel oracle has limits the prompt half-sees. It diffs only the text I chose to render, so a tooltip, a print stylesheet or a hover state with its own font-feature-settings can regress unseen. Text rasterisation differs between Chromium on Linux, macOS and Windows, so zero diff on one machine is a proof about that machine. And "any visual change is a regression" needs a line drawn somewhere: my weight trim moved one glyph by 157 grey levels. Whether that counts is a judgement the prompt correctly hands back to the owner.
What I would change in the prompt
- Add a rule to read hinting before touching it. On the Google Fonts builds I tested, there was none to drop. Make the measurement step decide.
- Make "the punctuation most pages use" a computed list, from the pages' own character frequencies, and report how many pages still fetch the extended file. On this site it was 83 percent with the prompt's list.
- Say "keep all OpenType features that any
font-feature-settings,font-variant-*or utility class turns on". The default feature list dropstnum,caseandzero, and the cost is visible. The saving from dropping the rest is real (Inter 121,672 B to 90,052 B), so it is worth the grep. - Treat a limited axis range as a reported deviation, not a silent one. It is cheap to approve, and it is not lossless.
- Add a "no glyph left behind" check and a rename check: fail on any character no shipped face covers, and list the name records that still contain a Reserved Font Name.
What I did not run
I did not fetch @shuding's blog or his numbers, so I cannot compare; Inter at 350,516 B to 30,664 B is 91 percent off the font bytes. I ran Chromium on Linux only: no Safari, Firefox or Windows rasteriser. None of these fonts ships an italic, so "bold inside italics" was only exercised through synthetic italic. I did not run the prompt through an agent end to end; I read the numbers a competent agent would be shown, as its reviewer. Throttling was Chromium's CDP emulator, not a real network. The closing ideas (inlining the core in the global stylesheet, precaching the extended files at idle) I did not test.
The prompt is a good one: goal, oracle, constraints, report is how to brief an agent on any task with a checkable outcome, and its warnings are real. Ask the agent for the deviation report, not just the green tick.