Fast font loading comes down to four moves: tell the browser about the font early (preconnect or preload), send small WOFF2 files that hold only the characters you need, choose a font-display value that suits the text, and size the fallback font to match so nothing jumps. Use font-display: optional when avoiding layout shift matters most.
| Value | Text while loading | Late font arrives | Good for |
|---|---|---|---|
| swap | Fallback almost immediately | Always swapped in | Body text that must show at once |
| optional | Fallback almost immediately | Not swapped in; fallback stays | Lowest layout shift |
| fallback | Fallback almost immediately | Swapped in only within about 3s | A middle ground |
| block | Invisible for up to about 3s | Always swapped in | Icon fonts, logos set in type |
| auto | Browser decides | Browser decides | Leaving it to the browser |
Updated
Fonts are one of the few resources that can hide your text or shove the layout around. Drag the slider below to set how long a font takes to arrive and watch what each font-display value shows at every moment, then copy the loading patterns that keep pages steady.
Starting view: the font takes 1.5 seconds to load; drag the slider to change it. Grey is invisible text, amber the fallback, blue the web font.
Scale: 0 to 5 seconds. With optional, the spec lets the browser abort a late download or finish it at very low priority.
| Value | Periods |
|---|---|
| block | Short block (3s recommended), infinite swap |
| swap | Extremely small block (100ms or less), infinite swap |
| fallback | Extremely small block, short swap (3s recommended) |
| optional | Extremely small block, no swap period |
as, type and crossorigin must all be present. The @font-face rule sits inline in the head so it is discovered at once.
<link rel="preload" href="/fonts/inter-var-latin.woff2" as="font" type="font/woff2" crossorigin>
<style>
@font-face {
font-family: "Inter";
src: url("/fonts/inter-var-latin.woff2") format("woff2");
font-weight: 100 900;
font-display: swap;
}
body { font-family: "Inter", system-ui, sans-serif; }
</style>Both rules share a family name. A page written only in English never downloads the Cyrillic file.
@font-face {
font-family: "Brand";
src: url("/fonts/brand-latin.woff2") format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+2013-2014, U+2018-201E;
font-display: swap;
}
@font-face {
font-family: "Brand";
src: url("/fonts/brand-cyrillic.woff2") format("woff2");
unicode-range: U+0400-045F, U+0490-0491;
font-display: swap;
}The numbers are placeholders: measure your own font against the fallback and tune them until the two line up.
@font-face {
font-family: "Inter Fallback";
src: local("Arial");
size-adjust: 107%;
ascent-override: 90%;
descent-override: 22%;
line-gap-override: 0%;
}
body { font-family: "Inter", "Inter Fallback", sans-serif; }The second stylesheet asks only for the letters in the logo text, which keeps that file tiny.
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Source+Sans+3:wght@400;700&display=swap">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Fraunces:wght@700&text=Harbour%20Bakery&display=swap">
When text needs a font that has not arrived, the browser walks through up to three periods. In the block period it lays the text out with an invisible fallback. In the swap period it shows the fallback and switches when the font lands. After that comes the failure period, where the fallback stays for good.
font-display only changes how long the first two periods last. The figures in the simulator are the ones the CSS Fonts Level 4 draft recommends; browsers may use slightly different timings.
Even invisible text takes up space measured with the fallback font. When the real font replaces it, every line can get wider or narrower, so paragraphs rewrap and everything underneath moves. That movement is counted in Cumulative Layout Shift.
There are two cures. Avoid the late swap entirely with optional, or make the fallback occupy the same space as the web font using size-adjust and the metric override descriptors on a local() fallback face.
A font with thousands of glyphs is wasteful for a page that uses a hundred. Subsetting tools strip unused characters before you upload the file, and unicode-range lets the browser skip whole files it does not need.
For a short piece of display text, the Google Fonts API accepts a text= parameter and returns a file with just those characters. It is handy for a logo or a single heading set in a decorative face.
It gives the font an extremely small block period, which the CSS Fonts spec recommends keeping at 100ms or less, and an infinite swap period. Text appears in the fallback font almost at once and switches to the web font whenever it finishes loading.
It can be. If the fallback and the web font have different widths or line heights, the late switch moves text and the content below it. Fix that with size-adjust and the ascent, descent and line-gap overrides on the fallback, or use optional.
Fonts are fetched in anonymous CORS mode, so the preload link needs the crossorigin attribute even when the file is on the same origin. Without it the preload does not match the real request and the font is fetched again.
Usually not. web.dev warns that preloading bypasses some of the browser's own content negotiation, including unicode-range, so you might fetch a subset the page never uses. Preconnect to fonts.googleapis.com and fonts.gstatic.com instead, or self-host and preload your own file.
When a @font-face rule has a unicode-range, the browser only downloads that file if the page uses at least one character in the range. Split a font into Latin, Cyrillic or Greek files and each visitor fetches only the scripts on the page.
Related: Core Web Vitals guide, PageSpeed Insights explained, Font pairing tool, CSS text styling.
Start for free or see all free tools. We.Inc is free to start, with no card needed. AI building is on paid plans from $20 a month.
We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.