# Web fonts and page speed: load one face, and load it first

You want clear type without slowing the first paint or nudging content. This guide gives you one fast way to load fonts and the trade-offs for small sites. You will ship a stable hero, a readable page and a pass on Core Web Vitals, without a week in type theory.

Updated 2026-09-14 · Source: https://porteur.ai/guides/web-fonts-and-page-speed

## Why fonts delay paint and cause shifts

A web font is render blocking the moment it sits in your first text paint. The browser must discover the font in CSS, request it, and wait. If you block render on that file, First Contentful Paint slips. If you do not, the browser draws with a fallback then swaps later. That swap can move lines and trigger Cumulative Layout Shift.

On a small product site, the hero is often the Largest Contentful Paint: a big heading or a poster image. If the hero text uses a late font, LCP waits. If the font swaps in with different metrics, CLS grows. Your aim is simple: one font face, discovered early, loaded early, and a fallback that matches its metrics so the swap does not move anything.

> Rule: load one face above the fold, and load it first. Defer every other face below the fold or after interaction.

## Pick a path: system stack, one self hosted variable WOFF2, or a host

Decide first. Fonts are a product choice, not just a build detail. Here is the order, best first for speed and simplicity.

| Path | What it is | When to choose | Speed impact | Notes |
| --- | --- | --- | --- | --- |
| System font stack | Use platform default faces like -apple-system, Segoe UI, Roboto | You want zero font bytes and instant paint, brand is fine with native look | Fastest | No downloads, no CLS from swaps, consistent with apps |
| One self-hosted variable WOFF2 | One variable font file on your domain, WOFF2, subsets per range | You need a brand face and a spread of weights | Very fast when preloaded | One DNS, one TLS, HTTP/2 reuse, replaces multiple files |
| Third-party host | Load from a font CDN like Google Fonts or a foundry CDN | You cannot self-host due to licence or policy | Slower | Extra DNS, connection and TLS before bytes, control is limited |

For most small sites, the sweet spot is one self-hosted variable WOFF2 plus a clean system fallback stack. You keep brand control, ship one file, and avoid a network hop to a third-party origin.

## font-display: swap or optional

Your font can block render, or it can let the fallback draw and swap later. You control this with font-display. For product pages, pick between swap and optional.

- swap: the browser shows fallback immediately, then swaps to the web font when it arrives. Good for readable first paint and steady UX when your fallback metrics match.
- optional: the browser may never swap if the font is late or the connection is slow. Good for speed first, minor visual variance on slow links.
- avoid auto or block for above-the-fold text: they can delay first paint or cause a long flash of invisible text.

```css
@font-face {
  font-family: "AcmeVar";
  src: url("/fonts/AcmeVar.woff2") format("woff2-variations");
  font-display: swap; /* or optional */
  /* font-weight range declared if needed by your setup */
  font-style: normal;
}

body { font-family: "AcmeVar", system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }
```

> Warning: swap without matched metrics can raise CLS. Fix the fallback metrics before you ship swap.

## Preload the one face above the fold

Preload tells the browser about a needed resource early. Do this for the one face used in your first viewport. It cuts discovery time. It helps LCP when the hero is text. Only preload what is definitely used on the first paint, or you waste priority.

```html
<!-- In <head> before your CSS link -->
<link rel="preload" as="font" href="/fonts/AcmeVar.woff2" type="font/woff2" crossorigin>
<link rel="preconnect" href="https://yourproduct.com" crossorigin> <!-- usually implicit for same-origin -->
```

If your first text sits inside a hero that uses a specific axis value, specify that weight in your CSS so the browser knows it will use the same file. You do not need separate files for each weight when you use a variable WOFF2.

## Subset with unicode-range to ship fewer bytes

Most fonts carry far more glyphs than your site needs. You can split the file per script and load less on first paint. Use unicode-range with a subset file for Latin, and keep extended sets lazy. This cuts transfer and parse cost.

```css
/* Latin subset for above-the-fold */
@font-face {
  font-family: "AcmeVar";
  src: url("/fonts/AcmeVarLatin.woff2") format("woff2-variations");
  font-display: swap;
  /* declare weight range if your build needs it */
  font-style: normal;
  unicode-range: U+0-7F; /* Basic Latin */
}

/* Extended subset loaded when needed */
@font-face {
  font-family: "AcmeVar";
  src: url("/fonts/AcmeVarExt.woff2") format("woff2-variations");
  font-display: optional;
  /* declare weight range if your build needs it */
  font-style: normal;
  unicode-range: U+0100-017F, U+0180-024F; /* example ranges only */
}
```

Generate subsets with your type vendor or a font tool. Keep one Latin subset for your default language, measured on real copy. Avoid creating many tiny files that fight for priority. One small subset for first paint is enough on most sites.

## Match fallback metrics to stop the swap from moving text

When you show fallback text then swap, the line height and glyph width must match. If not, the swap can push content and raise CLS. Fix this with CSS font metric overrides on the fallback. You set ascent, descent, line gap and size adjustment so the layout stays the same after the swap.

```css
/* 1. Choose a close fallback face */
body { font-family: "AcmeVar", system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }

/* 2. Override its metrics to match AcmeVar */
@font-face {
  font-family: "FallbackMetrics";
  src: local("Arial");
  size-adjust: 1;           /* example only, set from your font */
  ascent-override: 0.9;     /* example only, set from your font */
  descent-override: 0.25;   /* example only, set from your font */
  line-gap-override: 0;     /* example only, set from your font */
}

/* 3. Use the adjusted fallback ahead of system stack */
body { font-family: "AcmeVar", "FallbackMetrics", system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }
```

You get the target metrics from the font vendor or a tool. Test the swap on a throttled link and watch for movement. Your fixed page shows no jump when the face arrives, and your CLS stays under 0.1 across sessions.

## Worked example: from two slow faces to one fast face

Say your homepage at yourproduct.com uses two families: FancySans regular and bold from a third-party CDN, plus FancySerif regular for quotes. The CSS pulls three separate WOFF files with no preload. The hero H1 is the LCP and uses FancySans bold. Lighthouse on Mobile shows a late first paint and a layout jump in the hero. The field report in Search Console shows LCP above 2.5 s at the 75th percentile and CLS at 0.18.

1. **Pick the path** Drop FancySerif. Replace FancySans with one self-hosted variable WOFF2, AcmeVar, covering light through bold.
2. **Host and subset** Place /fonts/AcmeVarLatin.woff2 on your origin. Generate a Latin subset for first paint. Keep the full set for later.
3. **Declare with swap and unicode-range** Define @font-face for the Latin subset with font-display: swap and a unicode-range for Latin.
4. **Preload the face** Add <link rel="preload" as="font" href="/fonts/AcmeVarLatin.woff2" type="font/woff2" crossorigin> to <head> before CSS.
5. **Set the stack and weight** Use body { font-family: "AcmeVar", system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; } and H1 { font-weight: bold; }.
6. **Match fallback metrics** Add a metrics-override fallback based on Arial. Adjust size and metrics to match AcmeVar headings.
7. **Defer the rest** Load the extended glyph set with font-display: optional. Do not preload it. Only faces used below the fold load later.

```html
<!-- Before: third-party, multiple files, no preload -->
<link href="https://fonts.examplecdn.com/css2?family=FancySans&display=swap" rel="stylesheet">
<link href="https://fonts.examplecdn.com/css2?family=FancySerif&display=swap" rel="stylesheet">

/* After: one self-hosted variable WOFF2 */
<link rel="preload" as="font" href="/fonts/AcmeVarLatin.woff2" type="font/woff2" crossorigin>
<link rel="stylesheet" href="/css/site.css">
```

Result on a mid-range phone: the hero paints with fallback immediately, then swaps with no movement. LCP improves because the face is discovered early and comes from your origin. CLS drops below 0.1 because the fallback metrics match. Lighthouse’s lab score rises, and the Core Web Vitals field assessment trends to pass after 28 days when 75th percentile LCP sits at or under 2.5 s. Scores vary between runs, so read the field section first in PageSpeed Insights and the Core Web Vitals report in Search Console.

## Implementation details that save seconds

- Keep @import out of your CSS for fonts. Put @font-face in your main CSS and preload the file.
- Inline critical CSS for the above-the-fold layout. Fonts still need a separate request, so preload the file too.
- Do not lazy-load fonts. Load one face early. Load the second face only when content that uses it enters the viewport or after load.
- Avoid data URIs for fonts. They block CSS delivery and cannot be cached across pages as well as a file.
- Minify and cache. Serve WOFF2 with long cache-control and immutable. Bump the file name on change, for example AcmeVarLatin.hash.woff2.
- Use HTTP/2 or HTTP/3. Your CSS and font share the same connection and arrive faster from your origin.
- Use type="module" and defer scripts. Keep the main thread free so the swap paints fast and INP stays good.

> Rule: never lazy-load the hero image or the hero font. The first viewport needs both at high priority.

## How this ties to Core Web Vitals and Lighthouse

Fonts touch all three Core Web Vitals. LCP waits if your hero text uses a font discovered late or fetched from a slow origin. CLS rises when a swap changes metrics. INP suffers if render work blocks the main thread during the swap or hydration. Aim for LCP at or under 2.5 s, CLS at or under 0.1, and INP at or under 200 ms at the 75th percentile of real users over 28 days.

Use PageSpeed Insights to see both the field data and a lab run. The top section shows the Chrome User Experience Report data when there is enough traffic. That is what Search uses. The lower section is Lighthouse on a simulated mid-range phone over throttled 4G. The two can disagree. Field data wins. Lighthouse’s score weights Total Blocking Time 30 percent, LCP 25 percent and CLS 25 percent as of version 10 and later. Use the audits to spot render-blocking CSS, late preloads and third-party fonts.

Check Search Console’s Core Web Vitals report for your mobile URLs. It shows pass or fail at the Good thresholds on field data. Fixes take time to reflect. The report aggregates over 28 days and at the 75th percentile. A URL with too few visits falls back to origin-level data or shows nothing. Keep shipping improvements, then wait for the trend to settle.

## Third-party fonts and when you cannot self-host

Sometimes a licence or a brand team rules out self-hosting. You can still reduce delay. Preconnect to the font CDN. Use display=swap if the service allows it. Inline only the small CSS that declares the font. Avoid CSS that pulls many unused weights. If the host lets you subset, pick the basic Latin set for first paint and keep extended sets separate.

```html
<!-- Preconnect to third-party font host in <head> -->
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Acme&display=swap">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Acme&display=swap" media="print" onload="this.media='all'">
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Acme&display=swap"></noscript>
```

You still pay an extra DNS lookup, a new connection and a TLS handshake. That is why a self-hosted WOFF2 is faster. If you must use a host, keep the rest of the page light so the connection cost is hidden by other work. Match fallback metrics the same way to avoid CLS when the swap lands.

## Checklist: one fast font load on a product site

- Decide: system stack, or one self-hosted variable WOFF2, or third-party.
- Keep one face above the fold. Remove decorative faces from the hero.
- Self-host WOFF2 on your origin. Serve with long caching.
- Subset for Latin with unicode-range. Keep extended sets optional.
- Set font-display: swap for the above-the-fold face. optional for non-critical sets.
- Preload the above-the-fold WOFF2 with as=font and crossorigin.
- Match fallback metrics with size-adjust and ascent-override.
- Keep CSS critical inline. Defer scripts. Avoid @import for fonts.
- Verify in PageSpeed Insights: field data section trends to pass, lab run shows no render-blocking fonts.
- Watch Search Console Core Web Vitals over 28 days. Adjust if CLS or LCP slips.

## Questions

### Should I use WOFF or WOFF2 for performance?

Use WOFF2. It is smaller on the wire and supported in modern browsers. Keep a WOFF fallback only if your audience needs very old browsers. For a small product site in 2026, WOFF2 is enough in most cases.

### Is font-display: swap bad for SEO or UX?

No. swap improves the first paint by showing fallback text immediately. The risk is layout movement when the web font arrives. Fix that with fallback metric overrides so the swap does not move anything. Then CLS stays low and UX is steady.

### When should I choose optional over swap?

Use optional when speed must win even on slow links and a late visual change would distract. optional tells the browser it can keep the fallback if the font is late. Use it for non-brand text or extended glyph sets, not for your main brand heading.

### Do I need a variable font?

A variable WOFF2 replaces separate weight files. You fetch one file once, then use multiple weights from its range. That cuts requests and helps the cache. If you only use one weight, a single static WOFF2 is fine. If you use more than one, a variable file is usually faster.

### How do I know the preload worked?

Open the Network panel and reload. The font request should start early, before or alongside CSS images, and have high priority. In Lighthouse, the ‘Preload key requests’ audit should be clear for your font. In PageSpeed Insights, the ‘Eliminate render-blocking resources’ audit should not list a late font.

### Does font loading affect rankings?

Google uses Core Web Vitals as a ranking signal at the Good thresholds on field data. Fonts can hurt LCP and CLS. Fixing font loading helps you pass the assessment. Relevance and content still matter more, and the Lighthouse score itself is not a ranking factor.

## Read next

- [Core Web Vitals for a product site: the three numbers](https://porteur.ai/guides/core-web-vitals): The product founder’s guide to LCP, CLS and INP: what Google measures, why phones decide the pass, common causes, and how to test and fix.
- [Largest Contentful Paint: what slows it and what fixes it](https://porteur.ai/guides/largest-contentful-paint): See what usually counts as LCP, what makes it slow, how to fix it in order, and how to read it in Lighthouse and field data.
- [How to fix cumulative layout shift, cause by cause](https://porteur.ai/guides/how-to-fix-cumulative-layout-shift): See how CLS is measured, find the shifting elements, fix each common cause, and check field data after 28 days to confirm it worked.
- [Render-blocking resources: what blocks the first paint and how to unblock it](https://porteur.ai/guides/render-blocking-resources): Stop CSS and synchronous scripts from blocking first paint. Read the Lighthouse audit and fix in order: defer, inline critical CSS, split CSS, and handle fonts.
- [Image optimisation for page speed: the hero image and everything below it](https://porteur.ai/guides/image-optimisation-for-page-speed): Fix the hero image first for LCP, then lazy-load the rest. Choose AVIF or WebP, set srcset and sizes, and add width and height to stop shift.
- [How to read PageSpeed Insights: field data first, then the score](https://porteur.ai/guides/how-to-read-pagespeed-insights): Start with field data, not the score. Learn what Google uses, why scores vary, and how to turn a PageSpeed Insights run into a clear order of fixes.
- [Lazy loading images: what to lazy-load and what never to](https://porteur.ai/guides/lazy-loading-images): Lazy loading images saves bandwidth. Here is what to lazy-load, what never to, how to handle iframes and backgrounds, and how it affects LCP and SEO.

Want to see if your hero font is blocking paint or shifting layout? Paste your homepage URL and get three concrete findings in about thirty seconds. Free check: https://porteur.ai/
