DEV Community

Cover image for Solved: The Practical Guide to Optimizing @font-face
Darian Vance
Darian Vance

Posted on • Originally published at wp.me

Solved: The Practical Guide to Optimizing @font-face

🚀 Executive Summary

TL;DR: Web fonts often cause Flash of Unstyled Text (FOUT) and Cumulative Layout Shift (CLS) due to delayed loading, negatively impacting user experience and Core Web Vitals. To solve this, optimize @font-face by preloading critical self-hosted fonts using to prioritize fetching, or use font-display: swap for a quick fix, effectively taming layout shifts.

🎯 Key Takeaways

  • The font-display: swap; property offers a quick fix for FOUT by immediately displaying a fallback font, but it does not eliminate the subsequent CLS.
  • Using in the HTML is the recommended permanent solution for prioritizing critical self-hosted web font downloads, effectively preventing both FOUT and CLS.
  • The crossorigin attribute is crucial when preloading fonts, even for self-hosted ones, to prevent browsers from fetching the font file twice due to CORS requirements.

Tired of your site flashing ugly system fonts on load? This is a no-nonsense guide for DevOps and front-end engineers to finally tame @font-face, stop layout shifts, and get your web fonts loading like they should.

Let’s Talk About @font-face: A DevOps War Story on Taming FOUT and CLS

I remember a launch a few years back. It was for a major client, one of those “all hands on deck, CEO is watching” situations. The marketing team had just signed off on a gorgeous, but hefty, new brand font. The front-end team implemented it, and everything looked pixel-perfect on their local machines. Then we pushed it to staging. The site loaded, and for a solid second and a half, every heading was in Times New Roman before snapping into the brand font. The whole page would reflow. It looked broken, cheap. My pager lit up with alerts from our synthetic monitoring tools: “Cumulative Layout Shift (CLS) score has degraded by 300% on prod-web-01”. It was a nightmare. This, my friends, is the all-too-common pain of poorly optimized web fonts.

The “Why”: The Browser is Doing Exactly What You Told It To

This isn’t a bug; it’s a feature, a really annoying one. The root of the problem is the browser’s rendering process. It’s a race. The browser parses your HTML, sees a link to your CSS, and starts fetching it. While that’s happening, it starts painting what it can. It reads your CSS and sees font-family: 'Super-Brand-Font', sans-serif;. The browser says, “Okay, I don’t have ‘Super-Brand-Font’ yet, so I’ll use the fallback sans-serif for now. No big deal.” It renders the page. Then, a second later, the actual WOFF2 font file finally downloads from asset-cdn-prod-01. The browser then shouts, “I’ve got it!” and dutifully re-renders all the text, causing that jarring flash and layout shift. You’re seeing a Flash of Unstyled Text (FOUT) and it’s killing your Core Web Vitals.

So, how do we fix it? We have a few tools in our belt, ranging from a quick fix to a full-on architectural change.

Solution 1: The Quick Fix – Just Tell It to Swap

The fastest thing you can do is add a single property to your @font-face declaration: font-display: swap;. This is you explicitly telling the browser, “Look, I know this is going to take a second. Just use the fallback font immediately, and when the real one arrives, swap it in.”

@font-face {
  font-family: 'Open Sans';
  src: url('/fonts/OpenSans-Regular-webfont.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap; /* <-- THIS IS THE MAGIC */
}
Enter fullscreen mode Exit fullscreen mode

The Good: It’s one line of code. It ensures your content is readable immediately. For many, this is “good enough.”

The Bad: It doesn’t solve the FOUT or the CLS. It just accepts it. You’re still going to see that flash; you’re just controlling the behavior slightly better. It’s a band-aid, not a cure.

Solution 2: The Permanent Fix – Preload Your Critical Fonts

If you want to actually solve the problem, you need to change the browser’s priorities. The browser doesn’t know your font file is critical until it parses the CSS that requests it. We can give it a hint way earlier by using <link rel="preload"> in the <head> of your HTML.

This tells the browser, “Hey! Stop what you’re doing. This font file is extremely important for the initial render. Go fetch it. Now.”

<!-- In your HTML's <head> -->
<link rel="preload" href="/fonts/Super-Brand-Font-Bold.woff2" as="font" type="font/woff2" crossorigin>
Enter fullscreen mode Exit fullscreen mode

Pro Tip: The crossorigin attribute is crucial here, even if you are self-hosting the font. Fonts are fetched using an anonymous mode CORS request, and preloading needs to match this behavior, or the browser will fetch the font twice! I’ve seen teams burn hours debugging that one.

This approach works best when you are self-hosting your fonts. If you’re pulling from a third-party like Google Fonts, you’re adding DNS lookups, TLS handshakes, and another point of failure. Download those WOFF2 files, put them on your own CDN, and preload them. This gives you control and is almost always my recommended approach.

Solution 3: The “Nuclear” Option – Subset and Inline

Sometimes, even preloading isn’t fast enough. Maybe you have a hero heading with “Welcome!” that absolutely, positively cannot shift. For this, we get serious. This is the “I need zero CLS above the fold, and I don’t care what it takes” option.

The process has two steps:

  1. Subset the font: A full font file contains hundreds of characters you don’t need for a single heading. Use a tool (like glyphhanger) to create a tiny version of the font that ONLY contains the characters you need (e.g., W-e-l-c-o-m-!). The file size will be minuscule.
  2. Inline the font: Convert that tiny font file to a Base64 string and embed it directly in your CSS inside a <style> tag in the HTML <head>.
<!-- In your HTML's <head> -->
<style>
@font-face {
  font-family: 'Super-Brand-Hero';
  src: url(data:font/woff2;base64,d09GMgABAAAAAAbcAA...[very long string]...) format('woff2');
  font-weight: 700;
  font-style: normal;
  font-display: block; /* Use 'block' here to wait a tiny moment for it to be ready */
}
</style>
Enter fullscreen mode Exit fullscreen mode

The Good: The font is available *instantly* with the HTML. There is no separate network request. The browser has it before it even needs it. This guarantees zero font-related CLS for that specific text.

The Bad: This is a maintenance nightmare. If the text changes, you have to repeat the process. It also increases the size of your initial HTML document, which can delay the first paint if you overdo it. This is a scalpel, not a hammer. Use it only for the most critical, visible-on-load text.

My Final Take

For 90% of cases, self-hosting your fonts and preloading the critical weights (e.g., regular and bold) is the sweet spot. It gives you a massive performance win without the maintenance headache of inlining. Start there. Don’t let a fancy font be the reason your beautifully engineered application feels slow and broken.


Darian Vance

👉 Read the original article on TechResolve.blog


☕ Support my work

If this article helped you, you can buy me a coffee:

👉 https://buymeacoffee.com/darianvance

Top comments (0)