Back to Blog

Custom fonts make a website feel branded and polished. They are also one of the most common performance killers we encounter during site audits. A single poorly loaded font family can add 500 milliseconds or more to your page render time, push your Largest Contentful Paint score into the red, and create an ugly flash of unstyled text that makes your site look broken.

The good news is that font loading is a solved problem. There are well-established strategies that let you use the fonts you want without sacrificing speed. Here is how we approach font loading on every project we build. Anyone starting from scratch will get more out of a full breakdown of Core Web Vitals for site owners.

Why Custom Fonts Cause Problems

When a browser encounters a custom font in your CSS, it has to download that font file before it can render any text that uses it. Depending on how the font is loaded, the browser will do one of two things while it waits:

FOIT: Flash of Invisible Text

The browser hides the text entirely until the font loads. The user sees blank space where your headline or body copy should be. If the font takes a few seconds to load, your page looks empty. This is the default behavior in most browsers.

FOUT: Flash of Unstyled Text

The browser displays the text immediately using a fallback system font, then swaps in the custom font once it loads. There is a brief visual shift, but at least the user can read the content right away.

Neither is ideal, but FOUT is almost always the better trade-off. Users can start reading immediately, and the swap is usually subtle enough that most people never notice it. FOIT, on the other hand, makes your site feel slow and broken.

The font-display Property

The simplest and most impactful thing you can do is set font-display: swap on your @font-face declarations. This tells the browser to show the fallback font immediately and swap in the custom font when it is ready.

css
@font-face {
  font-family: 'BrandFont';
  src: url('/fonts/brandfont.woff2') format('woff2');
  font-display: swap;
}

If you are using Google Fonts, you can add &display=swap to the URL:

html
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">

This single change eliminates FOIT and improves perceived performance significantly. We treat it as a non-negotiable baseline on every project.

The fastest sites ship less to the browser, not more.
The fastest sites ship less to the browser, not more.

Preloading Critical Fonts

Even with font-display: swap, the browser does not start downloading your font until it encounters the CSS that references it. If your CSS file is large or loaded late, the font download gets delayed.

Preloading tells the browser to start downloading the font file immediately, before it even parses the CSS:

html
<link rel="preload" href="/fonts/brandfont.woff2" as="font" type="font/woff2" crossorigin>

A few important details:

  • Only preload the fonts used above the fold. If you preload every weight and style of your font family, you are actually hurting performance by competing for bandwidth.
  • The crossorigin attribute is required for font preloads, even if the font is self-hosted on your own domain. Without it, the browser will download the font twice.
  • Use WOFF2 format. It has the best compression and is supported by every modern browser.

We typically preload one or two font files: the regular weight used for body text and the bold weight used for headlines. That covers the vast majority of above-the-fold content.

Subsetting Fonts

Most font files contain glyphs for every character in every language the font supports. If your site is in English, you are shipping thousands of characters you will never use. A full-featured font file might be 100KB or more. Subset it to Latin characters only, and it might drop to 20KB.

There are several ways to subset fonts:

  • Google Fonts does this automatically when you specify &subset=latin in the URL (though this is now the default behavior).
  • For self-hosted fonts, tools like glyphhanger or fonttools (pyftsubset) let you generate subsets based on the actual characters your site uses.
  • Online tools like Font Squirrel’s Webfont Generator let you upload a font and choose which character ranges to include.

We have seen subsetting reduce font file sizes by 60 to 80 percent. On a site loading four font files, that can mean savings of 200KB or more, which is a noticeable improvement on mobile connections.

System Font Stacks as Fallbacks

The fallback fonts you specify matter more than most people realize. If your custom font is “Inter” and your fallback is the browser default serif font, the swap from serif to sans-serif will be jarring and cause a significant layout shift.

A good system font stack closely matches the metrics of your custom font:

css
font-family: 'Inter','Inter Fallback', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;

Even better, CSS now supports size-adjust, ascent-override, descent-override, and line-gap-override properties that let you fine-tune your fallback font to match the dimensions of your custom font almost exactly. This minimizes or eliminates Cumulative Layout Shift during the font swap:

css
@font-face {
  font-family: 'Inter Fallback';
  src: local('Arial');
  size-adjust: 107%;
  ascent-override: 90%;
  descent-override: 22%;
  line-gap-override: 0%;
}

Frameworks like Next.js handle this automatically with their font optimization module. If you are building with vanilla CSS, tools like Fontaine or the @font-face descriptor generator from Google can calculate these values for you.

Self-Hosting vs Google Fonts CDN

For years, the standard advice was to use Google Fonts via their CDN because browsers would cache fonts shared across sites. That advantage disappeared when browsers started partitioning their caches by domain for privacy reasons. A user who visited another site using the same Google Font will no longer have it cached for your site.

With cache partitioning in place, self-hosting is now the better choice in most cases:

  • Fewer DNS lookups and connections. Loading from Google Fonts requires a DNS lookup to fonts.googleapis.com, then another to fonts.gstatic.com. Self-hosting eliminates both.
  • Full control over caching. You can set aggressive cache headers on your self-hosted font files (they rarely change).
  • No third-party dependency. If Google’s CDN is slow or blocked (which happens in some regions and on some networks), your fonts still load.
  • Privacy. Self-hosted fonts do not send your users’ IP addresses to Google. This matters for GDPR compliance and was the subject of a notable German court ruling.

To self-host Google Fonts, you can use the google-webfonts-helper tool, which generates the @font-face CSS and provides the font files for download.

We self-host fonts on almost every project now. The setup takes about ten minutes, and the performance and privacy benefits are worth it every time.

Variable Fonts

Traditional font families require a separate file for each weight and style. A typical setup might include regular, bold, italic, and bold italic, which is four files. If you add a light weight and a semibold, that is six files.

Variable fonts combine all of these into a single file. One file contains the entire range of weights (and sometimes widths, slants, and other axes). This means:

  • Fewer HTTP requests. One file instead of four or six.
  • Smaller total file size. A variable font is usually larger than a single static font file but smaller than the combined size of multiple static files.
  • Design flexibility. You can use any weight value (like font-weight: 450), not just the predefined weights.
css
@font-face {
  font-family: 'Inter';
  src: url('/fonts/Inter-variable.woff2') format('woff2-variations');
  font-weight: 100 900;
  font-display: swap;
}

Most popular fonts now offer variable versions. Inter, Roboto Flex, Open Sans, and many others are available as variable fonts. If you are using more than two weights of the same font family, switching to the variable version is almost always a net performance win.

Measuring Font Impact on LCP

Largest Contentful Paint (LCP) is one of Google’s Core Web Vitals, and fonts directly affect it. If your largest above-the-fold element is a text block (which it often is), the font load time is a direct component of your LCP score.

Here is how to measure it:

  • Lighthouse (in Chrome DevTools or PageSpeed Insights) will flag render-blocking font requests and show your LCP time.
  • WebPageTest gives you a waterfall chart where you can see exactly when font files start and finish downloading relative to other resources.
  • Chrome DevTools Performance tab lets you record a page load and see the exact moment text becomes visible.
  • The Network tab with “Font” filter shows you every font file your page requests, its size, and how long it took to download.

We test font performance on throttled connections (simulating a 3G mobile connection) to catch issues that are invisible on fast office internet. A font load that takes 50 milliseconds on fiber might take 800 milliseconds on a slow mobile connection, and that is exactly the scenario where FOIT becomes painfully visible.

How to Reduce Font Load, in Order of Payoff

Everything above explains the individual techniques. This is the order to apply them in, from the change that removes the most bytes to the change that removes none but keeps the page readable while it waits. Most sites we audit have this backwards: font-display gets set first because it is one line of CSS, and the four families and eleven weights underneath it never get touched. A typical WOFF2 weight runs roughly 15 to 30KB, so four families at several weights each is how a site ends up shipping a few hundred KB of font data before a single word is readable.

Work down the list and stop when the remaining steps stop earning their time:

  1. Cut the families and weights you are not using. Write down every family and weight the design actually renders, then compare that to what the CSS requests. The gap is almost always bigger than anyone expects. Deleting one unused family at three weights removes 45 to 90KB and costs nothing but a line of CSS.
  2. Subset whatever survives. Now that the list is short, cut each remaining file down to the characters the site actually sets. This is the second largest reduction available and the reader never sees a difference.
  3. Self-host the WOFF2 files. Serving them from your own domain takes two third-party lookups off the critical path and puts the cache headers under your control.
  4. Preload only the faces used above the fold. Usually that is two: the body regular and the heading weight. Preloading more turns a priority hint into bandwidth competition and slows down the exact files you were trying to hurry.
  5. Set font-display on every face. No bytes saved, but the text becomes readable while the download finishes.
  6. Match a fallback’s metrics to the real face. This is what stops the swap from shoving the rest of the layout down the page.
  7. Replace icon fonts with inline SVG. On most sites the last font file left is not type at all.

Where the Big Bytes Actually Are

Only the first two steps delete data, and the arithmetic is worth doing by hand. Two families at two weights each, subsetted, is four files in the 15 to 30KB range, so 60 to 120KB of type total, which a phone on a weak signal absorbs without drama. The same site with four families at five weights each is requesting twenty files, and nothing further down the list rescues that. You cannot preload your way out of it.

That makes step one a design decision before it is an engineering one. Picking three display faces because they looked good in the mockup is the moment the problem gets created, which is why we settle the type system early on custom web design projects instead of trimming it back after launch when the design is already approved.

Why the Timing Steps Come Later

Steps three through five change when the bytes arrive, not how many of them there are. That is worth doing, but it is second-order. Preloading a 300KB font set does not make it smaller. It moves the stall a little earlier in the load, and if you preload a face you later delete, you paid twice: once for a wasted download, once for bandwidth stolen from the file that mattered. Trim the list first and every preload tag, cache header, and fallback metric you write is one you write only once.

The Font Nobody Counts: Icons

Icon fonts are the step teams forget, because nobody thinks of an icon set as typography. It behaves like typography anyway: another file, another request, another blocking dependency, and it ships the entire library so the page can draw a phone icon and a hamburger. When it loads late or fails, users get empty boxes instead of readable fallback text.

Inline SVG ships only the shapes you place on the page, needs no separate request, inherits color through currentColor, and scales cleanly. Pull the handful of icons you use out of the set, paste them into the markup, and delete the font. That is one fewer file on the critical path for no visual change at all.

A finished pass usually looks like this: two families at most, subsetted, self-hosted as WOFF2, one or two preloads, swap set on everything, a fallback whose metrics match, and no icon font anywhere. If your list still runs longer, the fix is higher up than you think.

A Practical Font Loading Checklist

Here is the approach we use on every project at Hunter Creative Media:

  • Limit font families. Use one, maybe two font families. Every additional family adds weight and complexity.
  • Limit weights. Load only the weights you actually use. If your design uses regular and bold, do not load light, medium, and semibold “just in case.”
  • Use WOFF2. It is the smallest format and has universal modern browser support.
  • Set font-display: swap. Always.
  • Preload critical fonts. One or two files for above-the-fold content.
  • Subset to Latin (or whatever character sets you actually need).
  • Self-host. Eliminate third-party dependencies and extra DNS lookups.
  • Use variable fonts when loading multiple weights of the same family.
  • Match fallback metrics. Use size-adjust and related properties to minimize layout shift.
  • Test on slow connections. Your users are not all on gigabit fiber.

The Compound Effect

Font optimization alone will not make a slow site fast. But we regularly see it contribute 200 to 500 milliseconds of improvement to LCP, which is significant when Google’s “good” threshold is 2.5 seconds. Combined with image optimization, efficient JavaScript loading, and server-side improvements, font optimization is an essential piece of the performance puzzle.

We recently worked on a site that was loading six font files (three weights in regular and italic) from Google Fonts CDN with no preloading and no font-display setting. The fonts alone were adding 1.2 seconds to the render time on mobile. After switching to self-hosted variable fonts with preloading and swap, we cut that to under 200 milliseconds. LCP went from 4.1 seconds to 2.3 seconds. That is the difference between failing and passing Core Web Vitals.

Keep Reading

Font discipline is part of every performance pass we do. See how the whole system came together in our Hot Dog Business Growth case study.

About the Author

Brian Hunter is the founder of Hunter Creative Media & Design, an Aurora, IL studio building hand-coded websites and custom web applications for small businesses across Chicago’s western suburbs. He writes about web performance, local SEO, and the software that runs small businesses. Get in touch.