completefrontend
Open lab My progress
THE WEB PERFORMANCE FIELD GUIDE

How web fonts load

Why a web font is found late, what visitors see while it downloads, how the five font-display values decide it, and how to load fonts from JavaScript.

By the end, you can
  • 01
    Follow a font to the screenExplain why the browser asks for a web font only after the HTML, the stylesheet and the text that needs it.
  • 02
    Choose a font-display valueSay what each value shows while the font downloads, and how long Chrome, Firefox and Safari wait.
  • 03
    Load fonts from JavaScriptStart a font with FontFace, follow its status, and know what document.fonts.ready and check() tell you.

Words that waited for their font

Ravi wants the Leaf and Kettle website to look like the shop. In the shop, the specials board is written in chalk, in round handwriting. So he picks Caveat, a free handwriting font, for the mint tea page. It's for the heading, the words about the tea, and the brewing steps. The shop's name at the top stays in the phone's own font. Phones don't come with Caveat, so the page has to download it.

He copies the CSS from a blog post. It ends with font-display: block. That line tells the browser what to show while the font downloads. The post says block makes sure the words never show in the wrong font. Asha opens the page on her phone, on a slow mobile link. The shop's name appears. Under it, nothing. A second later, all the words arrive at once, in Caveat.

Ravi changes one word: swap instead of block. Now the words come with the page, in her phone's own font, and turn into Caveat a second later. Why did one word make so much difference? And why did the font take so long?

The course loaded both pages three times in Chrome 157, on a phone's screen. It used DevTools' Slow 4G network, and made the CPU 4 times slower. It kept a picture of every frame Chrome drew.

When the words appearedPop out in the code editor (opens in a new tab)JavaScript
// When Asha saw the words on her phone, as recorded (ms after she opened the page)const block = { pageShown: 1234, wordsShown: 2234 };const swap = { pageShown: 1226, wordsShown: 1226 }; console.log(`block: the words came ${block.wordsShown - block.pageShown} ms after the page`);console.log(`swap: the words came ${swap.wordsShown - swap.pageShown} ms after the page`);
  • Line 1 is a comment. Times are in milliseconds (ms) after Asha opened the page. Of the three loads, these times come from the one whose font arrived in the middle.
  • Line 2 is the page with block: the page showed at 1,234 ms, and the words at 2,234 ms.
  • Line 3 is the page with swap: the page and the words both showed at 1,226 ms.
  • Lines 5 and 6 print block: the words came 1000 ms after the page and swap: the words came 0 ms after the page.

With block, Asha looked at the shop's name and an empty page for a whole second. The words were already there, in their places, but invisible. With swap, she could start reading at once. Here is what her screen showed:

The mint tea page on Asha's phone, on Slow 4G. font-display: block: 1,234 ms, only the shop's name; 2,234 ms, the words, in Caveat. font-display: swap: 1,226 ms, the words, in the phone's font; 2,221 ms, the words, in Caveat.First paintThe font arrivesfont-display: blockThe mint tea page at 1,234 ms: only the shop's name1,234 msonly the shop's nameThe mint tea page at 2,234 ms: the words, in Caveat2,234 msthe words, in Caveatfont-display: swapThe mint tea page at 1,226 ms: the words, in the phone's font1,226 msthe words, in the phone's fontThe mint tea page at 2,221 ms: the words, in Caveat2,221 msthe words, in Caveat
Asha's screen as Chrome drew it. With block, the words were there but invisible for a second. With swap, they came with the page in her phone's own font, then changed to Caveat.
Real-life analogyThe specials board and the chalk pens

Ravi writes the specials board every morning in chalk pen. Today the pens have run out, and new ones are in the post. He can leave the board blank until they come, so nobody sees it in the wrong pen. Or he can write it now in plain marker, and go over it in chalk when the pens arrive.

In real life: The chalk pens, still in the post
In fonts: The web font, still downloading
In real life: Leaving the board blank until they come
In fonts: block: the words are there, but invisible
In real life: Writing the board in plain marker first
In fonts: swap: the words in a font the phone already has
In real life: Going over it in chalk when the pens arrive
In fonts: The swap, when the font arrives
The main idea

A web font is the last link in a chain. First comes the HTML, then the stylesheet, then the text that needs the font. While it travels, font-display decides what the visitor sees. It's either invisible words, or the words in a font the device already has. The browser can't ask for the font any sooner on its own. So on a slow link, the wait is long.

The rest of the lesson takes four questions, one section each:

  • Why did the font come so late? Why fonts are discovered late.
  • What exactly did Asha see? Invisible text and swapped text.
  • What can font-display choose? The five font-display values.
  • Can JavaScript help? The font loading API.

For the same story without a stopwatch, see Web fonts in the system design course.

Why fonts are discovered late

A web font is a font file the page downloads, like a picture. Ravi's CSS tells the browser about it in two parts.

Ravi's font, described and usedCSS
@font-face {  font-family: "Caveat";  src: url("caveat.woff2") format("woff2");  font-display: swap;}h1, p, li { font-family: "Caveat", sans-serif; }
  • Lines 1 to 5 are an @font-face rule: a description of one font. Line 2 gives it a family name, "Caveat".
  • Line 3 says where the file is: caveat.woff2, next to the stylesheet. It also gives the format, WOFF2: a compressed format for fonts on the web. The next lesson covers it.
  • Line 4 is font-display. It comes back in the next two sections.
  • Line 6 uses the font. The heading, the paragraph and the list items ask for "Caveat", and if it isn't there, for the phone's sans-serif.

Notice what the rule doesn't do: it doesn't download anything. The font is fetched only when some text uses it. The CSS Fonts spec says the rule "is designed to allow lazy loading of font resources that are only downloaded when used within a document." A browser that fetched every font in a stylesheet, used or not, would break the spec.

So before it can ask for the font, the browser needs three things, in this order.

  • The HTML, to find the link to the stylesheet. The HTML never names the font, so the preload scanner can't find it early.
  • The stylesheet, to find the @font-face rule. It's render-blocking, so nothing is drawn before it arrives either.
  • Some text that uses the font. The browser learns that when it works out each element's styles. web.dev puts it this way: the browser "does not download the web font until it is determined that the current page's layout needs it."
Real-life analogyA recipe that sends you out for one spice

A friend posts you a recipe card. You can't buy the spices before the card arrives, because you don't know which ones you need. And you only go out for cardamom when you reach the step that uses it. If no step uses it, you never buy it.

In real life: The recipe card, still in the post
In CSS: The stylesheet, downloading
In real life: The spices the card lists
In CSS: The @font-face rules
In real life: Reaching the step that uses cardamom
In CSS: Text whose style asks for the font
In real life: The trip to the shop
In CSS: The font's download, which starts only then

Here is the swap page on Asha's phone, from the same recording:

The stylesheet, then the fontPop out in the code editor (opens in a new tab)JavaScript
// The swap page on Asha's phone, as recorded (ms after she opened the page)const stylesheet = { asked: 599, arrived: 1179 };const font = { asked: 1183, arrived: 2205 }; console.log(`The stylesheet took ${stylesheet.arrived - stylesheet.asked} ms`);console.log(`The font was asked for ${font.asked - stylesheet.arrived} ms after that`);console.log(`The font took ${font.arrived - font.asked} ms`);
  • Line 2 is the stylesheet. Chrome asked for it at 599 ms, when it read the link in the HTML, and it arrived at 1,179 ms.
  • Line 3 is the font: asked for at 1,183 ms, and in at 2,205 ms.
  • Lines 5 to 7 print The stylesheet took 580 ms, The font was asked for 4 ms after that and The font took 1022 ms.

The font couldn't start until the stylesheet had come. Then it needed a second of its own. A trace is Chrome's own record of every task it ran. Chrome's trace shows the exact moment it began loading the font. It was inside a Recalculate style task: the step where Chrome works out every element's styles. Chromium's source calls this "a fast path of loading web font in style phase". You met the same wait in Remove render delay, where a heading's font was asked for right after its stylesheet.

The swap page on Asha's phone. The stylesheet: asked for at 599 ms, arrived at 1,179 ms. The font: asked for at 1,183 ms, arrived at 2,205 ms. Asha saw nothing until 1,226 ms, her phone's font until 2,221 ms, then Caveat.The stylesheet599 ms1,179 msThe font1,183 ms2,205 msWhat Asha sawnothing yetphone's fontCaveat1,226 ms2,221 ms0 s1 s2 s
The font couldn't start until the stylesheet had arrived: Chrome asked for it 4 ms later. Asha saw her phone's font from 1,226 ms, and Caveat from 2,221 ms.

What if no text needs a font until later? Ravi adds a bold version of Caveat, in a file of its own. It's for a tip that appears a few seconds after the page.

A bold face, in its own fileCSS
@font-face {  font-family: "Caveat";  src: url("caveat-bold.woff2") format("woff2");  font-weight: 700;  font-display: swap;}

It's a second @font-face rule for the same family. Line 3 points at its own file, and line 4, font-weight: 700, says this face is the bold one. Then a script adds the tip:

Ravi's tip, three seconds inJavaScript
setTimeout(() => {  const tip = document.createElement("p");  tip.innerHTML = "<strong>Ravi's tip:</strong> add a slice of lemon.";  document.querySelector("main").append(tip);}, 3000);
  • Lines 1 and 5 wait 3,000 ms, counted from when the script runs.
  • Lines 2 and 3 make a paragraph that starts with <strong> words. Bold text in Caveat needs the bold face.
  • Line 4 adds the tip to the page.

The course recorded this page too. Chrome asked for the regular face at 1,186 ms, as before. It didn't ask for caveat-bold.woff2 until 4,229 ms, once the tip was on the page. For the first four seconds of the visit, nothing was fetched for the bold face, because nothing used it. Compare the versions here.

Playground: when each font is asked for
The requests
  1. styles.cssasked 599 ms, arrived 1,179 ms
  2. caveat.woff2asked 1,183 ms, arrived 2,205 ms
  3. Chrome began loading it insideTask > Recalculate style
Try it yourself
Font in the stylesheet

The font waited for the stylesheet, then for Chrome to work out which text needs it.

Recorded in Chrome 157, on a phone's screen, with a CPU 4 times slower. DevTools' Slow 4G. Times are in ms after Asha opened the page, from the load with the middle font time of 3.

Why it matters: a web font waits for the HTML, the stylesheet and the styles, one after another. On Asha's phone it was asked for at 1,183 ms. Whatever font-display says, the words can't be in Caveat before the font arrives. Starting it sooner is the next lesson's job, and the last section here shows one way.

Invisible text and swapped text

While the font travels, the browser has words to draw and no font to draw them in. It can do one of two things. Chrome's docs name what each looks like:

  • Invisible text. Hide the words until the font arrives. The docs call it "a flash of invisible text (FOIT)".
  • Swapped text. Show the words now in a font the device already has: the fallback font. Then swap the web font in when it arrives. The docs call that "a flash of unstyled text (FOUT)".
Real-life analogyCurtain down, or the understudy on stage

The lead actor is stuck in traffic. The theatre can keep the curtain down: the stage is ready behind it, but the audience sees nothing. Or it can send the understudy on now, and switch when the star arrives. The audience sees the play sooner, and also sees the switch.

In real life: The stage set up behind the curtain
In fonts: The words laid out, but invisible
In real life: The understudy, who knows the lines
In fonts: The fallback font
In real life: The star walking on in the middle of a scene
In fonts: The swap, when the web font arrives
In real life: Costumes that fit a little differently
In fonts: Lines that rewrap, and a layout shift

Invisible text isn't missing text. The CSS Fonts spec says the browser draws it with a fallback "with the same metrics as the selected font face but with all glyphs "invisible" (containing no "ink")". A font's metrics are its sizes, and its glyphs are its letter shapes. The words take up exactly the room the fallback font would. Everything around them is in its place; you just can't see them. You've met this before, as the sign under a sheet in Remove render delay. The icons waiting in their places in SVG and icons were the same idea.

Which fallback? The spec says the first font in the element's font-family list that's already loaded. Ravi's rule lists "Caveat", sans-serif, so Asha saw her phone's sans-serif.

Look at the opening picture again. The two pages differed in three ways:

  • When she could read. With block, at 2,234 ms. With swap, at 1,226 ms.
  • LCP. The largest text on the page was the paragraph. Invisible words don't count for LCP, so with block it came at 2,236 ms, when the paragraph became visible. With swap, it came at 1,224 ms.
  • Movement. Caveat is narrower than the phone's font. When it arrived, the first step fitted on one line instead of two, and the steps below it moved up. That's a layout shift. CLS, Cumulative Layout Shift, scores how much the page moved: with swap, the page's CLS was 0.01.

Block didn't avoid the shift. Its CLS was 0.0063, because the invisible words had been laid out in the fallback's sizes too. web.dev explains that block "requires laying out the page assuming the fallback fonts, even if the text itself is not shown". Making the fallback take up the same room as the font is the subject of lesson 20.3.

Why it matters: invisible text costs reading time and LCP, and swapped text costs a jump. Neither is free, but on a slow link, a second of blank page is the bigger cost for most text.

The five font-display values

font-display is the line in the @font-face rule that picks between invisible and swapped text, and for how long. The CSS Fonts spec describes it with a timer. It starts "At the moment the user agent first attempts to use a given downloaded font face". The user agent is the browser. In Chrome, that moment is when it begins loading the font. The timer runs through three periods.

  • The block period. If the font hasn't arrived, the words are invisible. If it arrives, it's used.
  • The swap period. If the font hasn't arrived, the words show in the fallback. If it arrives, it's swapped in.
  • The failure period. If the font still hasn't arrived, the browser gives up on it. The fallback stays for the rest of the page's life, even if the font comes later.
Real-life analogyThe rules for a late speaker

A club's speaker is late. The club has rules for this. Wait in silence for a few minutes. Then a colleague starts reading the speaker's notes, and the speaker takes over on arrival. After a while, the colleague just finishes. Each club picks its own timings, and one club simply books the speaker for next week.

In real life: The room waiting in silence
In CSS: The block period: invisible words
In real life: A colleague reading the speaker's notes
In CSS: The swap period: the fallback, and the font may still take over
In real life: The colleague finishing, even if the speaker turns up
In CSS: The failure period: the fallback for good
In real life: The speaker giving next week's talk instead
In CSS: optional: the font is in the cache for the next page

Each value sets how long the first two periods last. Read the table across: what the spec suggests for each period, then what Chrome's source code does.

The five values of font-display
ValueHow long are the words invisible?Until when can the font still swap in?What does Chrome do?
autoThe browser decides.The browser decides.Like block, but only until 2 seconds after the visit began. On 3G or slower, no invisible words at all.
blockAbout 3 seconds, the spec suggests.For as long as the page is open.Hides the words for up to 3 seconds after it begins loading the font.
swap100 ms or less, the spec suggests.For as long as the page is open.Never hides the words. The fallback shows at once.
fallback100 ms or less, the spec suggests.About 3 seconds, the spec suggests.Hides the words for 100 ms. After 3 seconds, it keeps the fallback for good.
optionalA moment at most: the font is used only if it can load "immediately", the spec says.No time at all: the font is used only if it's ready for the first paint.Never hides the words. It uses the font only if it was ready before the words were first painted.

Chrome's numbers come from two timers in its source, set to 100 ms and 3,000 ms. Both start when it begins loading the font. auto is whatever the browser prefers. The spec notes that "Many browsers have a default policy similar to that specified by block." Chrome adds two rules. They only apply to auto.

  • On a slow connection, no invisible words. If Chrome rates the connection 3G or slower, it shows the fallback at once. It also loads the font at its lowest priority. It tells the console: "Slow network is detected".
  • Otherwise, at most 2 seconds. The words stay invisible until 3 seconds after the font started, or 2 seconds after the visit started, whichever comes first. The source calls the second a limit "Based on the "good" LCP value of 2500ms."

All five, on Asha's phone

The five values on Slow 4GPop out in the code editor (opens in a new tab)JavaScript
// Asha's phone, Slow 4G: when the words showed, and when Caveat did (ms after she opened the page)const shown = {  auto: { words: 1241, caveat: 2224 },  block: { words: 2234, caveat: 2234 },  swap: { words: 1226, caveat: 2221 },  fallback: { words: 1313, caveat: 2229 },  optional: { words: 1230, caveat: null },}; for (const [value, { words, caveat }] of Object.entries(shown)) {  console.log(`${value}: words at ${words} ms, Caveat ${caveat === null ? "never" : `at ${caveat} ms`}`);}
  • Lines 3 to 7 hold the times. Each value has two: when the words became readable, and when they turned into Caveat.
  • Line 11 prints one line per value, from auto: words at 1241 ms, Caveat at 2224 ms to optional: words at 1230 ms, Caveat never.

Four findings:

  • auto behaved like swap. DevTools' Slow 4G adds 562.5 ms of delay to every request. Chrome rates a round trip of 272 ms or more as 3G. The page reported a 3g connection, and Chrome asked for the font at its lowest priority.
  • block hid the words for a second, until the font arrived. Its 3-second timer never ran out.
  • fallback hid them only briefly, from 1,247 to 1,313 ms, then showed the phone's font. The font came well inside 3 seconds, so it was swapped in at 2,229 ms.
  • optional never used Caveat. The font arrived after the words were first painted, so it wasn't used on this visit.

A font that comes very late

On Asha's phone, the font always came long before 3 seconds. To watch the timers run out, the course loaded the page again. This time it used DevTools' Fast 4G, which Chrome rates 4G. And the font's server waited 4 seconds before answering. The font then arrived at about 4.5 seconds.

How long the words stayed invisiblePop out in the code editor (opens in a new tab)JavaScript
// Fast 4G, the font's server 4 seconds slow (ms after the visit started)const late = {  auto: { fontAsked: 383, fallbackShown: 2010 },  block: { fontAsked: 378, fallbackShown: 3391 },  fallback: { fontAsked: 385, fallbackShown: 503 },}; for (const [value, { fontAsked, fallbackShown }] of Object.entries(late)) {  console.log(`${value}: invisible until ${fallbackShown} ms, ${fallbackShown - fontAsked} ms after the font was asked for`);}
  • Lines 3 to 5 hold when Chrome asked for the font, and when the words first showed in the fallback.
  • Line 9 prints auto: invisible until 2010 ms, 1627 ms after the font was asked for.
  • Then it prints block: invisible until 3391 ms, 3013 ms after the font was asked for.
  • Last comes fallback: invisible until 503 ms, 118 ms after the font was asked for.

block waited for its 3-second timer, plus the next frame. auto stopped at Chrome's 2-second limit, counted from the visit's start. fallback waited 100 ms, then showed the phone's font, and its swap period ended 3 seconds after the font was asked for. Caveat came after that, so it was never used. Here are the frames:

The heading in every frame Chrome drew on Fast 4G, with the font's server 4 seconds slow. auto: 424 ms, invisible words; 2,010 ms, the phone's font; 4,512 ms, Caveat. block: 421 ms, invisible words; 3,391 ms, the phone's font; 4,510 ms, Caveat. swap: 432 ms, the phone's font; 4,520 ms, Caveat. fallback: 430 ms, invisible words; 503 ms, the phone's font. optional: 427 ms, the phone's font.autoauto, at 424 ms: invisible words424 msinvisible wordsauto, at 2,010 ms: the phone's font2,010 msthe phone's fontauto, at 4,512 ms: Caveat4,512 msCaveatblockblock, at 421 ms: invisible words421 msinvisible wordsblock, at 3,391 ms: the phone's font3,391 msthe phone's fontblock, at 4,510 ms: Caveat4,510 msCaveatswapswap, at 432 ms: the phone's font432 msthe phone's fontswap, at 4,520 ms: Caveat4,520 msCaveatfallbackfallback, at 430 ms: invisible words430 msinvisible wordsfallback, at 503 ms: the phone's font503 msthe phone's fontCaveat came too lateoptionaloptional, at 427 ms: the phone's font427 msthe phone's fontCaveat came too late
The top of the screen in each frame, with the font held back 4 seconds. block hid the words for 3 seconds, auto until 2 seconds into the visit, and fallback for 100 ms. fallback and optional never used Caveat.

Other browsers use the same two lengths, a little differently. Firefox's settings gfx.downloadable_fonts.fallback_delay and fallback_delay_short are 3,000 and 100 ms: 3 seconds for auto and block, 100 ms for the others. So Firefox can hide the words for up to 100 ms even with swap, as the spec allows. WebKit, Safari's engine, gives auto and block 3 seconds, swap none, fallback 100 ms then 3 seconds, and optional 100 ms then nothing.

Pick a value to see its frames and numbers on both networks:

Playground: the five values, recorded
Asha's phone: Slow 4G
  1. The page at 1,241 ms: the phone's font1,241 ms: the phone's font
  2. The page at 2,224 ms: Caveat2,224 ms: Caveat
  1. Connection type Chrome reported3g
  2. The font's priorityLowest
  3. Font asked for1,185 ms
  4. Font arrived2,207 ms
  5. Words readable1,241 ms
  6. Words in Caveat2,224 ms
  7. LCP1,240 ms, <p>
  8. CLS0.01
A late font: Fast 4G, the font 4 s late
  1. The page at 424 ms: invisible words424 ms: invisible words
  2. The page at 2,010 ms: the phone's font2,010 ms: the phone's font
  3. The page at 4,512 ms: Caveat4,512 ms: Caveat
  1. Connection type Chrome reported4g
  2. The font's priorityHighest
  3. Font asked for383 ms
  4. Font arrived4,495 ms
  5. Words readable2,010 ms
  6. Words in Caveat4,512 ms
  7. LCP2,008 ms, <p>
  8. CLS0.01
Try it yourself
font-display: auto

Asha's phone: Slow 4G: Caveat from 2,224 ms. A late font: Fast 4G, the font 4 s late: Caveat from 4,512 ms.

Recorded in Chrome 157, on a phone's screen, with a CPU 4 times slower. Each strip is the load with the middle font arrival of 3. Times are in ms after the visit started.

And optional? It didn't throw the font away. The course opened the optional page a second time in the same browser. The font came from the cache, 0 bytes over the network. The words were in Caveat from the first paint, at 617 ms. That's the spec's plan for optional: "If they return later, the desired font faces might have finished downloading".

Try the timers yourself. Drag the slider to change when the font arrives.

Playground: Chrome's font timers
What the words show
  1. block0 ms to 1,000 ms: invisible words; from 1,000 ms: Caveat, for good
  2. swap0 ms to 1,000 ms: the phone's font; from 1,000 ms: Caveat, for good
  3. fallback0 ms to 100 ms: invisible words; 100 ms to 1,000 ms: the phone's font; from 1,000 ms: Caveat, for good
  4. optionalfrom the start: the phone's font, for good
Try it yourself
The font arrives at 1,000 ms

A model built from Chrome's two timers, 100 ms and 3 seconds, counted from when it begins loading the font. It isn't a recording: the playground above shows real frames.

Times count from when Chrome began loading the font. For optional, the model takes the font as ready for the first paint only when it's already in the cache.

Why it matters: a slow font can cost your visitors their words, their layout, or nothing but a nicer look. font-display decides which. Leaving it out doesn't mean "no waiting": it means the browser's own rules.

The font loading API

So far, CSS decided when the font loaded. The CSS Font Loading API lets JavaScript decide, and reports how it's going. Its parts:

  • document.fonts: the page's set of font faces. Each @font-face rule puts one in it.
  • FontFace: one font face, made from JavaScript, like an @font-face rule.
  • A face's status, which is "unloaded", "loading", "loaded" or "error", and its loaded promise, fulfilled when the font is ready.
Real-life analogyOrdering the chalk pens yourself, with a tracking number

This time, Ravi doesn't wait until he notices the board needs chalk. He orders the pens first thing in the morning. Then he follows the parcel on its tracking page: ordered, on its way, delivered. When they arrive, they go in the drawer with his other pens, where he'll look for them.

In real life: Writing the order
In JavaScript: new FontFace(): nothing is fetched yet
In real life: Sending it first thing in the morning
In JavaScript: load(): the download starts now
In real life: The parcel's tracking page
In JavaScript: status: unloaded, loading, loaded
In real life: Putting the pens in the drawer with the others
In JavaScript: document.fonts.add()
Making and loading the font from a scriptJavaScript
const caveat = new FontFace("Caveat", 'url("caveat.woff2") format("woff2")', { display: "swap" });document.fonts.add(caveat);console.log("Before load():", caveat.status);caveat.load();console.log("After load():", caveat.status, document.fonts.check("26px Caveat"));caveat.loaded.then(() => {  console.log("Loaded at", Math.round(performance.now()), "ms:", caveat.status, document.fonts.check("26px Caveat"));});
  • Line 1 makes a FontFace for the family "Caveat", from the same file, with display: "swap".
  • Line 2 adds it to document.fonts. MDN says it "must be added to the set before it can be used for drawing."
  • Line 3 prints its status. Making a face downloads nothing.
  • Line 4 calls load(), which starts the download now.
  • Line 5 prints the status again, and document.fonts.check(): whether text in 26px Caveat could be drawn now without a swap later.
  • Lines 6 to 8 wait for the font, then print the time, the status and check() once more.

Ravi put this script in the page's head, before the stylesheet's link. He also took the @font-face rule out of the stylesheet. On Asha's phone, the console printed Before load(): unloaded, After load(): loading false and Loaded at 1615 ms: loaded true.

The script asked for the font at 608 ms, while Chrome was still reading the HTML. The trace shows it began inside Evaluate script, the task that runs a script. The stylesheet only arrived at 1,175 ms. The font was in at 1,601 ms, against 2,205 ms when it waited for the stylesheet. Pick FontFace in the head in the playground in the second section to compare.

Waiting for all the fonts

Sometimes you want to know when all the fonts are done, for example before you measure some text. That's document.fonts.ready: a promise the spec fulfills "when the document is done loading fonts".

When are the fonts ready?JavaScript
document.fonts.ready.then(() => {
  console.log(`Fonts ready at ${Math.round(performance.now())} ms`);
});

Lines 1 to 3 print the time once the fonts are done. The course read ready twice on the page with Ravi's tip. It learned when to read it.

  • Read at the very start, before any font was asked for, it was fulfilled at 603 ms. Nothing was loading yet, so there was nothing to wait for.
  • Read right after the tip was added, it was fulfilled at 5,231 ms, once the bold face had arrived. Chromium's source explains why. Reading it makes Chrome work out styles and layout first. That "may trigger font loads, and replace |ready_| with a new Promise."

So read ready once the content is on the page. The spec warns it's "only fulfilled once, but further fonts may be loaded after it fulfills." Events help too. document.fonts fires a loading event when fonts start loading, and a loadingdone event when they finish. On the tip page, loadingdone came at 2,232 ms for the regular face and 5,231 ms for the bold one. But MDN's data says Safari never fires loadingdone, so ready is the safer choice.

Try the API in your own browser.

Playground: load Caveat from JavaScript
What happened
  1. Nothing yetPress the button
The words, in "Caveat Lab", sans-serif

Mint tea, fresh this week

Try it yourself
Press the button

This runs in your own browser, so the times are yours. A second try may be quicker: the file can come from your browser's cache. Reset removes the font face again.

The file is the course's Caveat, 74,932 bytes, from /nextjs-assets/fonts-in-nextjs/Caveat.woff2.

Why it matters: CSS finds a font late. A script can ask for it as soon as the HTML is read. It can also tell you exactly when the font has arrived.

Pick a font-display value for each text

For each piece of text, ask two questions. Does it make sense in another font? And should it move once a visitor is reading it?

Which font-display value fits?
  • An icon font, where the letter P draws a printer
  • The shop's name, set in its logo font
  • The mint tea heading, which Ravi wants in Caveat even if it comes late
  • The body text of a long article
  • Body text where the font is only a nice touch
  • Tea descriptions on a shop that visitors browse page after page
Try it yourself
0 of 6 correct

Sort each piece of text into the value that suits it.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Common mix-ups

  • "An @font-face rule downloads the font." No. The font is fetched only when some text uses it. The bold face waited until the tip came, at 4,229 ms.
  • "Invisible text takes no space." No. It's laid out in the fallback's sizes, with no ink. That's why block still shifted the page: CLS 0.0063.
  • "Without font-display, the words show at once." No. You get auto, the browser's own rules. With a fast connection and a slow font, Chrome hid the words until 2,010 ms.
  • "block hides the words until the font comes, however long that takes." No. After about 3 seconds, Chrome, Firefox and Safari show the fallback. With the late font, block showed it at 3,391 ms.
  • "With optional, the font is wasted." No. The font is kept in the cache. On the second visit, the words were in Caveat from the first paint.
  • "document.fonts.ready waits for every font the page will ever need." No. It's fulfilled when nothing is loading. Read at the very start, it was fulfilled at 603 ms, before any font was asked for.

Practice exercises

Answer each one first, then open the hints if you need them.

Exercise 1 · Warm-upblock, 3.5 seconds in

A font with font-display: block arrives 4 seconds after Chrome asks for it. What does the last line print?

Starter codePop out in the code editor (opens in a new tab)JavaScript
const blockPeriod = 3000;
const fontArrives = 4000;
const at = 3500;

const sees = at < blockPeriod ? "nothing" : at < fontArrives ? "the fallback" : "Caveat";
console.log(`At ${at} ms: ${sees}`);

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upDoes fallback swap?

    With font-display: fallback, the font arrives 2.5 seconds after Chrome asks for it. What does the last line print?

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    const swapPeriodEnds = 3000;
    const fontArrives = 2500;
    
    console.log(fontArrives < swapPeriodEnds ? "Caveat swaps in" : "The fallback stays");

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeLCP with a font that never came

      In the recording with the late font and font-display: fallback, when did LCP happen, in ms?

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeFind the missing line

        Ravi's console says Caveat loaded, but the words never change to Caveat. Which line is missing?

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        const caveat = new FontFace("Caveat", 'url("caveat.woff2") format("woff2")');
        caveat.load().then(() => {
          console.log("Caveat is", caveat.status);
        });

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · Challengecheck() and a font nobody has

          What does document.fonts.check("16px NoSuchFont") return?

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeFirefox's extra time

            In Firefox, block hides the words for 3 seconds. But if most of the font has arrived when those 3 seconds run out, Firefox waits a little longer. How much longer, in ms?

            Answer, then press Check. Spacing and letter case don’t matter.

              Check your understanding

              For each question, ask how far along its way the font was, and which period its timer was in.

              How web fonts load quiz · 7 questionsScore: first tries count
              1. Question 1 of 7What does this stylesheet-to-font code print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                const stylesheetArrived = 1179;
                const fontAsked = 1183;
                const fontArrived = 2205;
                
                console.log(`${fontArrived - stylesheetArrived} ms from stylesheet to font`);

                Choose an answer to see the explanation.

              2. Question 2 of 7In the recording, when did Chrome ask for the font from the stylesheet?

                Choose an answer to see the explanation.

              3. Question 3 of 7What does the browser draw during a font's block period?

                Choose an answer to see the explanation.

              4. Question 4 of 7In the recording on Asha's phone, why did font-display: auto behave like swap?

                Choose an answer to see the explanation.

              5. Question 5 of 7In the recording with the late font, which value kept the fallback even after Caveat arrived?

                Choose an answer to see the explanation.

              6. Question 6 of 7What does document.fonts.ready tell you?

                Choose an answer to see the explanation.

              7. Question 7 of 7What changed when Ravi made the font with new FontFace() in a script in the head?

                Choose an answer to see the explanation.

              Key takeaways

              • A web font is fetched only when some text needs it, after the HTML, the stylesheet and the styles. That makes it late.
              • While it travels, the words are either invisible, laid out but with no ink, or in a fallback font that swaps later.
              • font-display sets the block, swap and failure periods. Chrome's timers are 100 ms and 3 seconds.
              • auto is the browser's choice. Chrome hides the words at most until 2 seconds into the visit, and not at all on 3G or slower.
              • FontFace and load() start a font from JavaScript. document.fonts.ready tells you when the fonts the page needs now are in.

              Remember the one-liner.
              A web font comes last, after the HTML, the stylesheet and the text that needs it; font-display decides what the visitor sees meanwhile.

              Sources

              The facts on this page come from these standards, docs and source code:

              Coming next: Smaller, faster fonts, also in Loading content fast: LCP. WOFF2, subsets and preloading make the font arrive sooner, so there's less to hide or swap.

              CompleteFrontend Clear concepts. Working examples.