Notebook · Note 002 · Typography
Ten languages, three font files
When you open a site to ten languages, the first thing that breaks is the font budget. It did not break. But we paid another cost for Cyrillic and Arabic letters, and we did not hide it.
Founder, BLN Global · · 6 min read
Short answer
If the same three fonts came from Google Fonts, a Turkish page would download 250 KB across 6 files from two external origins. From our server, it downloads 165 KB across 3 files from one origin. Most of the saving comes from fixing unused axes, not subsetting. We had to choose separate families for Cyrillic and Arabic characters. In Arabic, the trade-off is that some symbols come from a fallback font.
The site's typography used three families: Archivo for headings, Instrument Sans for body text, Martian Mono for numbers and labels. All three were variable fonts. Five languages posed no problem. At ten, two issues grew at once: download size and script coverage.
What we compare with what
Comparing raw TTF files with the WOFF2 files we ship would not be fair. Most of the gap comes from compression, not subsetting. The useful comparison is with the real alternative: what would download if the same three families came from Google Fonts.
The browser does not download all nine files. It unicode-range field and fetches only the subsets the page needs. For a Turkish page, that means latin + latin-ext per family:
latin 88.0 KB <- latin-ext 83.8 KB <- vietnamese 33.6 KB latin 29.2 KB <- latin-ext 10.8 KB <- latin 23.0 KB <- latin-ext 15.5 KB <- cyrillic 10.3 KB cyrillic-ext 3.1 KB Turkish page download: 250 KB (6 files)
Font load on the Turkish page
archivo.woff2 + instrument.woff2 + martian.woff2 the actual size of the files on disk.Bytes were not the only win: six requests fell to three, and the DNS + TLS handshake to two external origins disappeared. How the render-blocking resource issue was closed in the last 18-point note is described.
Three traps
One: the latin-ext subset lacks basic Latin letters
On the first try, we downloaded and subset the Google Fonts latin-ext file. The result had 130 glyphs and no digits. The reason is simple: latin-ext is a suffix set layered on top of latin, not a complete alphabet by itself. Download the full variable font from the google/fonts repository and subset it yourself.
Two: if you do not pin unused axes, the file grows fivefold
Every variable font axis adds weight to the file. The design uses only weight and width. Unless you pin the other axes to their defaults, their data stays in the file.
full = {a.axisTag: a.defaultValue for a in t['fvar'].axes if a.axisTag not in axis} full.update(axis) # axes other than wght/wdth are pinned to default, # otherwise the file gets five times bigger
Three: gvar loads lazily
after pinning the axis with fontTools, when we tried to subset directly KeyError: space is returned. The cause is lazy loading of variable glyph data (gvar): after instancing, the object looks valid in memory, but the subsetter cannot read it. The fix is awkward but takes one line: save the file as a temporary TTF, reopen it, then subset it.
Cyrillic and Arabic: the price you pay
Archivo has no Cyrillic, and neither does Instrument Sans. Neither has Arabic. We had to choose separate families for these two languages, and we did not choose at random.
- Russian: Roboto Flex. That was the only structural match: it has a width axis. So all the CSS
font-variation-settingsvalues work unchanged. One file serves both heading and body, so the Russian page stays lighter than the Latin page. - Arabic: Noto Sans Arabic. The only candidate with a width axis. One 144 KB file fills all three roles, so the Arabic page downloads less than the Latin page.
Here is the cost: in Noto Sans Arabic → and ✓ icons are missing. On the Arabic page, those two marks come from a fallback font and look slightly different from their neighbors. We wrote it into the CSS as a comment instead of hiding it, because the next person will spot the same difference and think it is a bug.
Right-to-left layout was a separate job: arrow directions flipped, position properties changed to their logical counterparts (margin-inline-start, inset-inline-start), so the same CSS works in both directions.
A small tweak: why Martian Mono is optional
Two of the three families font-display:swap uses. Martian Mono optional. The reason was measured: with swap, Martian arrived late, remeasured the menu button, and caused a 0,015 layout shift. Since it is preloaded, it arrives in time anyway. In the one case it does not, the page stays put and keeps the fallback font.
The font folder totals 404 KB across ten languages. No visitor downloads it all. Each page fetches only its language's files.