Font Loading and Typography Performance

Keyboard shortcuts
  • JNext lesson
  • KPrevious lesson
  • /Search lessons
  • EscClear search

The average WordPress site loads 4-8 font files. Each file is 20-100KB. That’s 80-800KB of fonts downloading before your text is visible. On a mobile connection, that’s 200-500ms of waiting just for letters to appear on screen.

I’ve seen sites loading 12 font files. Twelve. Regular, bold, italic, bold italic, in two different font families, plus Font Awesome for icons. 1.2MB of fonts alone. On a site where the total HTML was 30KB.

Fonts are a hidden performance cost because they don’t show up as “the problem” in most audits. PageSpeed Insights doesn’t scream about font files the way it screams about large images. But they silently add to your load time, block text rendering, and create layout shifts when they swap in late.

This chapter cuts your font loading down to what actually matters.

Google Fonts: The Privacy and Performance Problem

Google Fonts is the most popular web font service. Over 60% of websites that use custom fonts load them from fonts.googleapis.com. It’s free, it has a massive library, and it’s easy to add.

It’s also a performance and privacy problem.

The Performance Issue

When your browser loads a Google Font, it makes two network connections:

  1. A connection to fonts.googleapis.com to get the CSS file that describes the font
  2. A connection to fonts.gstatic.com to download the actual font files

Each new connection takes time. DNS lookup, TCP handshake, TLS negotiation. On a fast connection, that’s 50-100ms per connection. On mobile 4G, it’s 100-300ms. And these connections happen before the font even starts downloading.

Then the font files download. On HTTP/2, multiple font files can download in parallel over the same connection, but you’re still waiting for the connection setup and the total transfer time.

The Privacy Issue

Every time a visitor loads Google Fonts from Google’s servers, Google receives the visitor’s IP address and browser information. In 2022, a German court ruled that using Google Fonts violates GDPR because it sends visitor data to Google without consent. Fines of 100 euros per violation were imposed.

Whether you care about GDPR enforcement depends on your audience. But the performance fix and the privacy fix are the same solution: self-hosting.

Self-Hosting Google Fonts: The Exact Workflow

Self-hosting means downloading the font files, putting them on your own server, and loading them from there. No external connections. No privacy concerns. Faster loading.

Here’s how I do it on every client site:

Step 1: Download the Fonts

Go to google-webfonts-helper.herokuapp.com (or use the gwfh.mranftl.com mirror). Search for your font. Select the weights you need (I’ll explain which ones in a moment). Select “Modern Browsers” for WOFF2 format only. Download the zip.

Alternatively, use the “google-webfonts-helper” tool by Mario Ranftl. It generates the CSS and the font files together.

Step 2: Choose Your Weights Carefully

This is where most people go wrong. They download every weight:

  • 300 (Light)
  • 400 (Regular)
  • 500 (Medium)
  • 600 (Semi-Bold)
  • 700 (Bold)
  • 300 Italic
  • 400 Italic
  • 700 Bold Italic

That’s 8 font files. Most sites only need 2-3. Here’s what I use:

Regular (400). For body text. You need this.

Bold (700). For headings and emphasis. You need this.

Italic (400 Italic). Only if your content regularly uses italics. Most blogs can skip this. The browser fakes italic from the regular weight, and the result is fine for occasional use.

That’s it. Two files for most sites. Three if you use italics heavily. Every additional weight you add is another 20-40KB of download time for minimal visual difference.

Step 3: Upload to Your Server

Create a folder in your theme: /wp-content/themes/your-theme/fonts/. Upload the WOFF2 files there.

If you’re using a child theme, put them in the child theme’s fonts folder. If your theme doesn’t have a fonts folder, create one.

Step 4: Add the CSS

In your theme’s stylesheet (or a custom CSS plugin), add @font-face declarations:

@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url('/wp-content/themes/your-theme/fonts/inter-v12-latin-regular.woff2') format('woff2');
}

@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 700;
    font-display: swap;
    src: url('/wp-content/themes/your-theme/fonts/inter-v12-latin-700.woff2') format('woff2');
}

Step 5: Remove the Google Fonts Connection

Find and remove the Google Fonts stylesheet link from your theme. It usually looks like:

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

If your theme loads it via PHP (in functions.php), you’ll find a wp_enqueue_style call with a Google Fonts URL. Dequeue it.

If you’re using FlyingPress, it has a “Self-host Google Fonts” option that does steps 1-5 automatically. Enable it, and FlyingPress downloads the font files, stores them locally, and rewrites the URLs. One toggle. I still prefer doing it manually for full control, but the FlyingPress automation works well.

Step 6: Preload Your Fonts

Add a preload hint in your <head> so the browser starts downloading fonts early, before it even parses the CSS:

<link rel="preload" href="/wp-content/themes/your-theme/fonts/inter-v12-latin-regular.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/wp-content/themes/your-theme/fonts/inter-v12-latin-700.woff2" as="font" type="font/woff2" crossorigin>

The crossorigin attribute is required for font preloading, even when the fonts are on your own domain. Browsers treat font requests as CORS requests by default.

Add these preload links via your theme’s wp_head action or through your caching plugin’s settings. FlyingPress has a dedicated “Preload fonts” section where you can paste the font URLs.

Font Display Swap

You’ve probably noticed the font-display: swap; in the @font-face declarations above. This is a critical property.

Without font-display: swap, when a browser downloads your page and encounters text styled with a custom font, it hides the text until the font file finishes loading. This is called FOIT (Flash of Invisible Text). On slow connections, visitors stare at blank text for 1-3 seconds.

With font-display: swap, the browser immediately shows text using a fallback system font, then swaps in the custom font once it loads. This is called FOUT (Flash of Unstyled Text). There’s a brief moment where the text changes appearance, but the content is readable from the first instant.

FOUT is better than FOIT. Always. Readable text with a font change is better than invisible text. Google Fonts includes display=swap by default now, and you should always include font-display: swap; in your self-hosted @font-face declarations.

Minimizing the FOUT

The font swap is more noticeable when the fallback font and the custom font are very different sizes. You can reduce this by choosing a system font fallback that closely matches your web font’s metrics:

body {
    font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}

Inter is close in metrics to the system sans-serif fonts, so the swap is minimal. If you’re using a font with unusual metrics (like a narrow condensed font), the FOUT will be more noticeable.

The tool “Fallback Font Generator” (screensiz.es/fallback) can generate a matched fallback font declaration that minimizes layout shift during the swap. It adjusts the fallback font’s size and spacing to match the web font. Useful for sites where CLS (Cumulative Layout Shift) from font loading is a problem.

Font Subsetting: Load Only What You Need

A standard Google Font file includes characters for many languages: Latin, Latin Extended, Cyrillic, Greek, Vietnamese, and more. If your site is in English, you only need Latin characters. Loading the full file means downloading glyphs you’ll never use.

How to Subset

Google Webfonts Helper lets you select specific character sets when downloading. Choose “Latin” only for English-language sites. This alone can reduce file size by 30-50%.

Glyphhanger (npm package) is the most powerful subsetting tool. It scans your actual website content and creates a font file containing only the characters that appear on your pages. If your site never uses the “Z” in a specific weight, it gets removed. Extreme, but effective for maximum optimization.

Font Squirrel’s Webfont Generator lets you upload a font file and customize the output. You can select character ranges, adjust hinting, and output in WOFF2 format.

For most sites, selecting “Latin” subset from Google Webfonts Helper is enough. Going further with Glyphhanger saves another 10-20KB per font file but requires more technical setup.

Variable Fonts: One File Instead of Four

Traditional web fonts require a separate file for each weight and style. Regular, bold, italic, bold italic. Four files minimum. If you add light and semi-bold, that’s six files.

Variable fonts pack all weights and styles into a single file. One file contains the entire range from thin to black, normal to italic. The browser renders any weight from that single file.

Why Variable Fonts Win

Fewer files. One WOFF2 file instead of four or more. Fewer HTTP requests, simpler loading.

Smaller total size. A variable font file is larger than a single static font file (40-80KB vs 20-40KB), but it’s smaller than four static files combined (40-80KB vs 80-160KB). You come out ahead as soon as you need more than two weights.

Design flexibility. You can use font-weight: 450 or font-weight: 550. Any value, not just the fixed 100/200/300/etc steps. This doesn’t matter much for performance, but it’s nice for design.

How to Use Variable Fonts

Most popular Google Fonts now have variable font versions. Inter, Roboto, Open Sans, Lato all have variable versions available.

Download the variable font WOFF2 file. Your @font-face declaration changes slightly:

@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 100 900;
    font-display: swap;
    src: url('/wp-content/themes/your-theme/fonts/inter-variable-latin.woff2') format('woff2');
}

The font-weight: 100 900 tells the browser this single file covers all weights from 100 to 900. You can then use any weight in your CSS: font-weight: 400 for body, font-weight: 700 for headings, even font-weight: 500 for a slightly heavier accent.

One file. One preload hint. One download. That’s it.

System Font Stacks: When to Skip Web Fonts Entirely

Here’s a question most performance guides don’t ask: do you actually need a custom font?

System fonts are the fonts already installed on every device. They load instantly because there’s nothing to download. Zero font files. Zero delay. Zero FOUT.

The system font stack that works everywhere:

body {
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif;
}

This stack gives you:

  • San Francisco on Apple devices (macOS, iOS)
  • Segoe UI on Windows
  • Roboto on Android
  • Appropriate fallbacks on Linux

All of these are clean, readable, modern sans-serif fonts. They look good. Your visitors are used to reading them because they see them in every app on their device.

When System Fonts Make Sense

Blogs focused on content over branding. If your content is the product, the font is secondary. System fonts let readers focus on words, not typefaces.

Performance-critical sites. If you’re chasing a perfect Core Web Vitals score, eliminating font files removes a variable entirely.

Sites with global audiences on slow connections. Every KB matters in emerging markets on 3G. Zero font files means zero font-related delay.

When Custom Fonts Are Worth the Cost

Strong brand identity. If your brand is built around a specific typeface, the performance cost is worth the brand consistency.

Design-heavy sites. Portfolios, agencies, and creative businesses where typography is part of the visual experience.

When the font is small. A single variable font file at 40KB, preloaded and self-hosted, adds maybe 20ms to load time on a good connection. That’s a reasonable cost for a distinctive look.

I use system fonts on my own sites. I switched two years ago and never looked back. My pages load with zero FOUT, zero CLS from font loading, and zero external connections for typography. For client sites, I usually self-host a single variable font with two weights maximum.

Font Loading Optimization: The Full Picture

Putting it all together, here’s the optimal font loading strategy:

Preload your font files so the browser starts downloading them immediately, not after it discovers them in the CSS.

Self-host to eliminate third-party connections.

Use font-display: swap so text is always visible.

Subset to Latin (or your language) to reduce file size.

Variable fonts to keep file count to one per family.

Preconnect (if you must use external fonts) to establish the connection early:

<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>

But self-hosting eliminates the need for preconnect entirely.

Resource Hints Order of Priority

If you’re preloading multiple resources, fonts should be near the top. The priority order I use:

  1. Critical CSS (inlined, not preloaded)
  2. Font files (preloaded)
  3. Hero image / LCP image (preloaded)
  4. Main JavaScript bundle (deferred, not preloaded)

Don’t preload everything. Browsers have a limited number of preload slots. If you preload too many resources, none of them get priority. Stick to 2-4 preloaded resources maximum.

My Font Strategy

After years of testing fonts on hundreds of sites, here’s what I’ve settled on:

Two font files maximum. One variable font for the primary typeface (body and headings). One additional file if I need a distinctly different accent font. That’s it. Two files. Usually under 80KB total.

Self-hosted. Always. No Google Fonts connections. No third-party DNS lookups, TCP handshakes, or TLS negotiations.

Preloaded. Both files get preload hints in the <head>. The browser starts downloading them immediately.

Subset to Latin. If the site is English-language, I strip out every character set except Latin. The file shrinks by 30-40%.

WOFF2 only. WOFF2 has 95%+ browser support. I don’t include WOFF or TTF fallbacks. The 5% of browsers without WOFF2 support get system fonts, and those browsers are so old that the user experience is already compromised in other ways.

This strategy loads fonts in 20-40ms on a decent connection. Compare that to the 200-500ms I see on sites loading 6-8 font files from Google’s servers. It’s a 5-10x improvement.

The Icon Font Tax

Font Awesome is the most popular icon font on the web. WordPress themes and plugins love it. And it’s slow.

Font Awesome loads a CSS file (30-60KB) and a font file (70-150KB depending on the version and icons used). That’s 100-200KB for icons. Icons that you’re probably using 5-10 of on any given page.

The problem is that icon fonts are all-or-nothing. You load the entire icon set even if you use three icons. Font Awesome 5 Free includes 1,500+ icons. You’re downloading all 1,500 to display a hamburger menu, a search icon, and a social media icon.

Inline SVG: The Better Way

Replace icon fonts with inline SVG. An inline SVG for a simple icon is 200-500 bytes. Ten icons as inline SVG? That’s 2-5KB. Compared to 100-200KB for Font Awesome.

Here’s a hamburger menu icon as inline SVG:

<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
    <line x1="3" y1="12" x2="21" y2="12"></line>
    <line x1="3" y1="6" x2="21" y2="6"></line>
    <line x1="3" y1="18" x2="21" y2="18"></line>
</svg>

422 bytes. Renders instantly. No external file to download. Styles with CSS using currentColor (inherits the text color). Scales perfectly at any size.

Where to Get SVG Icons

Heroicons (heroicons.com): Clean, well-designed icons from the Tailwind CSS team. MIT licensed.

Feather Icons (feathericons.com): Lightweight, consistent set. 280+ icons.

Lucide (lucide.dev): Fork of Feather with more icons and active development.

SVG Repo (svgrepo.com): Huge collection of free SVG icons from various sources.

Copy the SVG code directly into your theme templates. If you need to use icons in the WordPress editor, create a shortcode or a custom block that outputs the inline SVG.

Removing Font Awesome

If your theme loads Font Awesome and you want to remove it:

  1. Identify which icons are actually used on your site
  2. Find SVG equivalents from one of the sources above
  3. Replace Font Awesome icon classes with inline SVG in your theme templates
  4. Dequeue the Font Awesome stylesheet:
add_action( 'wp_enqueue_scripts', function() {
    wp_dequeue_style( 'font-awesome' );
    wp_dequeue_style( 'fontawesome' );
}, 20 );

The hook priority of 20 ensures your dequeue runs after the theme enqueues the style.

If a plugin loads Font Awesome and you can’t easily dequeue it (because the handle name is non-standard), use Asset CleanUp to disable it on pages where it’s not needed.

The first time I replaced Font Awesome with inline SVGs on a client site, the total page weight dropped by 180KB. The font loading time went from 250ms to zero. That’s a measurable improvement that visitors feel, even if they can’t articulate why the site feels faster.


Chapter Checklist

  • [ ] Google Fonts self-hosted (no external font connections)
  • [ ] Font files limited to 2-3 maximum (regular, bold, maybe italic)
  • [ ] Variable fonts used where available (one file instead of four)
  • [ ] Font subsetting applied (Latin only for English sites)
  • [ ] WOFF2 format only (drop WOFF/TTF fallbacks)
  • [ ] font-display: swap on all @font-face declarations
  • [ ] Font files preloaded in the <head>
  • [ ] System font stack considered for performance-focused sites
  • [ ] Font Awesome replaced with inline SVG icons
  • [ ] Total font payload under 80KB
  • [ ] No third-party font connections in the Network tab

Chapter Exercise

Audit your font loading right now:

  1. Open Chrome DevTools > Network tab > filter by “Font”
  2. Count the number of font files loading. Record their total size
  3. Check if any fonts load from external domains (fonts.googleapis.com, fonts.gstatic.com, use.fontawesome.com)
  4. If using Google Fonts externally, self-host them using the workflow in this chapter
  5. If loading more than 3 font files, identify which weights you can drop
  6. If using Font Awesome, count how many icons you actually use on the homepage
  7. Replace those icons with inline SVG and dequeue Font Awesome
  8. Add preload hints for your remaining font files
  9. Retest. Your font-related network requests should be 1-2 local files only

Target: 2 font files maximum, self-hosted, preloaded, totaling under 80KB. Zero external font connections. Zero icon font files. This is achievable on every WordPress site, and the performance difference is real.

Disclaimer: This site is reader-supported. If you buy through some links, I may earn a small commission at no extra cost to you. I only recommend tools I trust and would use myself. Your support helps keep gauravtiwari.org free and focused on real-world advice. Thanks. - Gaurav Tiwari