Technology · · 7 min read
Self-Hosting Your Fonts
No Google Fonts, no third-party requests, and icon fonts subset from 75 KB down to 1.5 KB. Here's how, and why it's worth an afternoon.
Self-Hosting Your Fonts
The text you're reading is set in Lora, the menus and buttons around it are in Poppins (no, not Mary Poppins), and until recently both arrived from Google's servers, the way fonts arrive on roughly half the web. Now they're served from this site itself: nine small font files sitting in the repository next to the photos. This post is just a brief rundown on how and why I did this, and how I shrank three icon fonts by about 96% with a Python script. Part of the series on how this site is built.
Quick jargon guide
- Web font: a font file the browser downloads with the page, so visitors see your chosen typeface rather than whatever their device has installed.
- WOFF2: the modern compressed format for web fonts. Universally supported, roughly a third the size of older formats.
- @font-face: the CSS rule that tells the browser "this family name maps to this font file". Self-hosting is mostly writing these by hand.
- Icon font: a font whose "letters" are pictures (the GitHub logo, a house, an arrow). A common way to ship icons before SVG took over.
- Subsetting: deleting every glyph from a font that your site doesn't use. A font with 700 icons of which you use 5 can lose the other 695.
- Unicode range: the span of characters a font file covers. Serving just the Latin range is itself a subset: no Cyrillic or Vietnamese glyphs for a site written in English.
The why
Well, mainly because I wanted to learn about fonts. I think they're interesting: they communicate tone, a certain visual 'feel' that goes beyond the words on the page, they have interesting accessibility benefits and they have some fascinating history. But, the technical reasons are: Privacy: every page view against Google Fonts tells Google's servers a visitor's IP address requested your page, and in 2022 a German court found that arrangement violated the GDPR. Serving fonts yourself means your readers talk to nobody but you. Reliability and speed: a third-party font server is one more thing that can be slow, blocked (Google is unreachable in some countries), or discontinued. Self-hosted fonts arrive over the same connection as everything else. Ownership: the same philosophy as the rest of this site. Fewer moving parts owned by other people.
The counterargument used to be caching: the theory that everyone has Google's copy of Poppins cached already, so linking to it is free. That theory died in 2020 when browsers partitioned their caches per site for privacy reasons. Every site now downloads its own copy regardless, so the shared-cache benefit is actually negative, since you download it repeatedly.
The how
Google Fonts provides them, of course. Download the family, keep the WOFF2 versions of the weights you actually use, and write an @font-face block per file:
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('../fonts/vendor/poppins-400.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'Lora';
font-style: normal;
font-weight: 400 700;
font-display: swap;
src: url('../fonts/vendor/lora-var.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
This site carries seven weights of Poppins and two files of Lora, upright and italic (the italics matter for pull quotes), Latin subset only, for a grand total of about 130 KB, roughly one photograph's thumbnail-and-hero pair. Lora is a variable font: one file holds every weight from 400 to 700, which is what font-weight: 400 700 declares. Google hands you that same file whichever Lora weight you ask for, so my first font sheet pointed at three copies of one file, and a page using two weights downloaded it twice. The font-display: swap line is the other non-obvious part: it tells the browser to show text immediately in a fallback font and swap when the real one arrives, rather than showing nothing. Neat, right? Invisible text while a font loads is way more irritating than a brief flash of the wrong font that most people won't see.
That flash came with a jump, at first. Georgia and Arial set the same words to different widths than Lora and Poppins, so when the real font landed every paragraph rewrapped and the page moved. So I added two more families, 'Lora Fallback' and 'Poppins Fallback', which are just Georgia and Arial scaled and re-measured (a size-adjust, plus ascent, descent and line-gap overrides) to take up the same space as the fonts they stand in for. The widths are weighted by how often each character turns up in this blog's own posts, about 736,000 characters of them. Each fallback goes straight after its web font in the font stack.
The fun part: subsetting icon fonts
The site's template arrived with three icon fonts (FontAwesome, Themify, ElegantIcons), each carrying hundreds of icons. A count of what the site actually uses came to: four FontAwesome icons, thirty-one Themify icons, and two ElegantIcons.
Using the fontTools library, a Python script scans every file git tracks for icon class names (fa-github, ti-arrow-left, and so on, found by the same scan that prunes unused rules from the stylesheet), parses each vendor stylesheet to map those class names to the character codes inside the font, and then writes a new font containing only those characters. The results are satisfying in a way few optimisations are:
# Original -> subset (woff/woff2)
# FontAwesome 75.4 KB -> 1.4 KB (4 icons kept, ~98% smaller)
# Themify 54.8 KB -> 4.4 KB (31 icons kept, ~92% smaller)
# ElegantIcons 62.2 KB -> 1.1 KB (2 icons kept, ~98% smaller)
The subset files live alongside the originals, and the CSS build swaps the font sources to the subsets at minification time, so the originals remain on hand for the day a new icon appears. That day is the obvious failure mode (use a new icon, forget to re-subset, icon renders as an empty box), so the script also writes a manifest of what's included, and the CSS build fails outright if the site uses an icon that isn't in it. I have some checks in place to make sure this goes smoothly, and that will be in another blog on how "my website has a test suite".
A note on SVG icons
Would SVG icons be more modern than icon fonts? Yes, and a fresh site should use them. But the template came with icon fonts wired into everything, and seven kilobytes of subset font is not a problem worth a rewrite. So, why not optimise the thing you have.
Common questions
Is self-hosting Google's fonts legal?
Yes. Fonts on Google Fonts are under open licences, mostly the SIL Open Font License, which explicitly permits serving them yourself.
How much faster is it, really?
Modest and consistent: you save a DNS lookup and TLS handshake to a third domain, which is tens to a couple of hundred milliseconds on first visit. The bigger wins are the icon subsets (about 190 KB of fonts became 7 KB) and immunity to the third party ever being slow.
Do I need all those weights?
Almost certainly not, and this site is mildly guilty: seven weights of Poppins is what the template's CSS references, so seven weights it is.
What about variable fonts?
A variable font packs every weight into one file. It's the right choice for a new build, and Lora already is one: two files, upright and italic, cover every weight from 400 to 700. Poppins has no variable release, so it stays as seven static weights, and the total is already small.