
Every custom font loaded from a third-party service costs your visitor a DNS lookup, a TLS handshake, and a render-blocking round trip before the text on your page can paint in the right typeface. Self-hosting fonts collapses that chain down to a single request to your own origin, one that your cache and CDN already know how to serve fast. It also stops sending every visitor’s IP address to a font provider on every page load, which matters if you care about privacy or run a site with EU visitors. Here is how to do it properly: pick the right files, subset them, self-host them, and preload only the one that actually affects your Largest Contentful Paint.
Why the third-party request actually costs you
When a browser hits a page that references an external font service, it has to discover the stylesheet, parse the @font-face rules inside it, then open a connection to a second domain (often a third, for the actual font file) before it can render text in that font. Every one of those hops adds latency on top of your own server’s response time. If the affected text is your hero heading or your main body copy, that delay shows up directly in Largest Contentful Paint. If the fallback font has different letter spacing or line height than the web font, you also get a visible reflow when the real font finally arrives, which shows up as Cumulative Layout Shift. LCP should stay at or under 2.5 seconds and CLS at or under 0.1, and font loading is one of the more common places sites quietly blow both budgets.
Only download the weights you actually use
Font families often ship with eight or nine weights and italic variants. Most sites use two or three: a regular weight for body text, a bold or semibold for headings, and maybe an italic. Download only what your design actually calls for. Every extra weight is another file the browser might have to fetch and another few kilobytes sitting in your page’s critical path.
Subset the files to the characters you need
A full font file usually includes glyphs for Cyrillic, Greek, various symbol sets, and other scripts your site never displays. Subsetting strips those out and can shrink a file significantly. The standard open source tool for this is fontTools, which installs with pip:
pip install fonttools brotli
pyftsubset SourceSans3-Regular.ttf \
--output-file=SourceSans3-Regular-latin.woff2 \
--flavor=woff2 \
--layout-features='*' \
--unicodes="U+0000-00FF,U+0131,U+0152-0153,U+2018-201E,U+2020-2022,U+2026,U+2039-203A,U+2044,U+2074,U+20AC,U+2122"Adjust the Unicode ranges to match the languages your site actually serves. If you only need basic Latin, a plain U+0000-00FF range covers standard Western European characters and is enough for most English-language sites. Repeat the command for each weight and style you kept.
Write clean @font-face rules
Once you have your subsetted .woff2 files, write the face declarations yourself instead of pulling in a generated stylesheet you don’t control. Put this in your child theme’s stylesheet or a dedicated fonts CSS file:
@font-face {
font-family: "Source Sans";
src: url("/wp-content/themes/your-child-theme/fonts/SourceSans3-Regular-latin.woff2") format("woff2");
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Source Sans";
src: url("/wp-content/themes/your-child-theme/fonts/SourceSans3-SemiBold-latin.woff2") format("woff2");
font-weight: 600;
font-style: normal;
font-display: swap;
}Use font-display: swap for text that needs to be readable immediately, the browser paints a fallback font first and swaps in your web font once it loads. For purely decorative type that isn’t part of the main content, font-display: optional lets the browser skip the swap entirely if the connection is slow, which avoids a layout shift at the cost of occasionally showing the fallback permanently on that page load.
Preload the one font that affects your LCP
Preloading tells the browser to fetch a resource early, before it would normally discover it through the CSS. This only helps if you’re selective. Preload the single font file used by your above-the-fold heading or hero text, the one most likely to be your LCP element. Preloading every weight on the page just adds competing requests and can slow things down instead of speeding them up.
<link rel="preload" href="/wp-content/themes/your-child-theme/fonts/SourceSans3-Regular-latin.woff2" as="font" type="font/woff2" crossorigin>In WordPress, add this through your child theme’s functions.php so it’s hooked into the document head early:
function serverborn_preload_hero_font() {
echo '<link rel="preload" href="' . get_stylesheet_directory_uri() . '/fonts/SourceSans3-Regular-latin.woff2" as="font" type="font/woff2" crossorigin>' . "\n";
}
add_action( 'wp_head', 'serverborn_preload_hero_font', 1 );Getting the files onto your site and confirming the change
Upload the .woff2 files into a fonts folder inside your child theme via SFTP, then enqueue the stylesheet that holds your @font-face rules the normal way, with wp_enqueue_style() in your theme’s setup function. Once the files are live, confirm your active theme and clear any cached CSS or HTML with wp-cli:
wp theme list
wp cache flushIf you’re running the LiteSpeed Cache plugin, it also exposes wp-cli purge commands, so a quick purge after a font swap guarantees visitors aren’t served a stale cached page still pointing at the old external font request.
Let caching do the rest
Static font files should be served with long, immutable cache headers since they rarely change once subsetted and versioned by filename. A LiteSpeed server handling full-page caching, paired with Redis for object caching, keeps your dynamic WordPress responses fast, while Cloudflare’s edge cache takes the font files themselves off your origin’s plate entirely after the first request. None of this requires special font-specific configuration, it just works because the files are now yours to cache instead of someone else’s to fetch. If you’re on a managed LiteSpeed host like ServerBorn, this caching layer is already sitting in front of your site, so self-hosted fonts get the same edge treatment as your other static assets without any extra setup.
Address the layout shift directly
If your fallback and web fonts have noticeably different metrics (character width, line height), you can narrow the visual jump during the swap with the size-adjust, ascent-override, and descent-override descriptors inside your @font-face rule. These let you tune the fallback font’s box dimensions to more closely match the web font’s, which reduces the reflow when the swap happens. This is a finishing touch worth doing after the basics are in place, once you’ve measured actual CLS in the field and found it’s still non-trivial.
Takeaway
Self-hosting fonts is one of the highest-leverage changes you can make to a WordPress site’s typography performance: fewer external origins, full control over font-display, the ability to preload exactly the file that matters for LCP, and one less third-party service quietly logging visitor IPs. Subset aggressively, preload sparingly, and let your existing cache and CDN layer handle delivery from there.