PerformanceGENERALBROWSER-SPECIFICNETWORK-SPECIFICDEVICE-SPECIFIC

Images and Fonts

The two heaviest things on most pages, and the two most often shipped at the wrong size, in the wrong format, discovered too late, and without any space reserved for them.

The intent, the obvious build, and why it breaks

Every lesson starts where the work starts: someone wanted an outcome, and the first implementation that comes to mind has a problem.

The question

How do I get the right pixels and the right glyphs to the user quickly, without moving anything that is already on screen?

The user intent

A person wants to look at a photograph and read some text. Both should be there, both should be sharp, and neither should arrive by pushing the other out of the way.

The obvious build

Export the images at high quality, reference the font from a stylesheet, and let the browser sort it out. It is good at this.

Why it breaks

A single source image sized for a desktop hero is delivered unchanged to a narrow phone screen, where the browser downloads several times the pixels it can display and then spends CPU scaling them down.

How it breaks in a real browser
  • A single source image sized for a desktop hero is delivered unchanged to a narrow phone screen, where the browser downloads several times the pixels it can display and then spends CPU scaling them down.
  • The font is referenced from inside a stylesheet, so its request cannot begin until that stylesheet has been fetched and parsed — a staircase that no amount of subsetting shortens (The Preload Scanner).
  • With no explicit dimensions, every image is a zero-height box until it decodes, and the page rearranges itself around each arrival (Visual Stability).
  • The default font behaviour hides text for a period and then shows it. On a slow connection the user is looking at a page of invisible words.
  • A "lightweight" icon font ships several thousand glyphs to render six of them, and blocks the text it sits next to while doing it.
  • Marking every image loading="lazy" delays the hero image specifically, because the hero is the one image the browser was already going to fetch first (Loading: Why Content Arrives Late).
IntentEventStateUI LogicDOM WorkNetworkLayout / PaintPixelsFeedback

What is actually happening

In the browser, not in the framework.

  • An image costs four things: bytes on the network, decode time turning them into a bitmap, memory to hold that bitmap at its decoded size, and layout if its box was not reserved. Only the first is what people usually measure.
  • Decoded memory is a function of pixel dimensions, not file size. A highly-compressed enormous photograph is a small download and a large bitmap (Reading Memory: RSS, Heap, Working Set and the Number on Your Dashboard in Observability).
  • srcset and sizes let the browser choose among candidates using the layout width and the device pixel ratio, which it knows and the server does not (Responsive Images).
  • Modern formats trade encode time and compatibility for size. The picture element expresses the fallback chain declaratively so the browser picks the first format it supports (Images, Video and the Elements That Own Their Layout).
  • loading="lazy" defers the request until the image is near the viewport; decoding="async" lets decode happen off the critical path; fetchpriority corrects the browser's priority guess. Three different mechanisms, frequently confused.
  • For fonts, the browser must decide what to draw before the file arrives. Hiding the text is FOIT; drawing a fallback and swapping is FOUT. font-display chooses between them, and there is no option that avoids both.
  • Subsetting removes glyphs the page cannot use, and unicode-range lets the browser skip downloading a subset entirely when no character on the page falls in its range.

What this makes the browser do

And which of it is avoidable.

  • Decoding, which for large images is significant and which the browser will do off the main thread when it can — decoding="async" is a request to prefer that.
  • Rasterising at the display size, and holding the decoded bitmap in memory for as long as the image is live. Many large images on one page is a memory problem before it is a bandwidth one.
  • Font matching and shaping: for each text run the browser resolves a family, falls back per character, and shapes glyphs. A long fallback chain costs matching work on every style recalculation (Style Calculation).
  • Re-layout on font swap, for every element using the family. On a text-heavy page that is close to a full-document layout (Visual Stability).
  • Avoidable: downloading pixels that cannot be displayed, downloading glyphs that cannot be rendered, and any layout caused by a box whose size was predictable.

FOIT or FOUT is a decision, not a default

Before a web font arrives, the browser has to draw something or draw nothing. Every option is a trade between showing text sooner and showing it in its final form, and font-display is where you state which trade you want. Leaving it unset does not avoid the decision; it accepts whichever default the browser applies.

The critical additional move — the one that makes this decision much less painful — is to adjust the fallback so it occupies the same space as the real font. Then a swap changes how the text looks without changing where anything is, and the option that shows text soonest stops carrying a stability penalty (Visual Stability).

What should the browser draw while the font is downloading?

Text needs to be rendered and the web font has not arrived. What do you want?

`swap` — fallback immediately, swap when ready

when Body text and anything the user came to read. Readability beats fidelity for content.

cost A visible change when the font arrives, and a reflow unless the fallback's metrics have been matched to the real font.

`optional` — fallback, and only use the font if it is already available

when Stability matters more than the typeface, or the font is nice-to-have branding on a repeat-visit-heavy site.

cost Some first-time visitors on slow connections never see the font at all, which is a design decision as much as a technical one.

`fallback` — briefly invisible, then fallback, swap only if quick

when A compromise for headings where a swap late in the page's life would be more jarring than not getting the font.

cost Combines a short invisible period with the possibility of never swapping — two partial downsides rather than one clear trade.

`block` — invisible until the font arrives

when A logotype or a short heading where the wrong glyphs would be worse than a brief absence, and the file is tiny and preloaded.

cost On a slow connection the user is reading blank space. Rarely correct for anything longer than a few words.

No web font for this text

when System font stacks are legible, already resident, and cost nothing. Body text in an application UI rarely needs a custom face.

cost Typography differs across platforms, which is a brand cost and, for some products, a real one.

The markup that reserves the space and picks the right file

Most of what makes images fast is expressed declaratively, which means the browser can act on it before any of your JavaScript has run. That is the whole reason to prefer markup here: the preload scanner can read it in the first chunk of the response.

Read the example for the four separate decisions it encodes: which format, which size, when to fetch, and how much space to hold. Each of those has been a separate production bug for somebody.

A hero image and a below-the-fold image, side by side
1<!-- Above the fold: eager, high priority, space reserved. -->
2<picture>
3 <source type="image/avif"
4 srcset="/hero-480.avif 480w, /hero-960.avif 960w, /hero-1440.avif 1440w"
5 sizes="(max-width: 700px) 100vw, 960px">
6 <source type="image/webp"
7 srcset="/hero-480.webp 480w, /hero-960.webp 960w, /hero-1440.webp 1440w"
8 sizes="(max-width: 700px) 100vw, 960px">
9 <img src="/hero-960.jpg"
10 width="960" height="540"
11 fetchpriority="high"
12 decoding="async"
13 alt="Two engineers reviewing a deployment on a shared screen">
14</picture>
15
16<!-- Below the fold: deferred, low priority, space still reserved. -->
17<img src="/chart-640.avif"
18 width="640" height="360"
19 loading="lazy"
20 decoding="async"
21 alt="Weekly deploy frequency rising steadily over six months">
22
23<!-- One font, the one the first screen renders in. -->
24<link rel="preload" as="font" type="font/woff2"
25 href="/inter-latin-400.woff2" crossorigin>

Four decisions, none of which need JavaScript: picture picks a format the browser supports, srcset plus sizes picks a resolution appropriate to the layout and the display, width/height reserve the box before the bytes exist, and loading/fetchpriority say which of the two images the browser should be in a hurry about. The crossorigin attribute on the font preload is not optional — without it the preload and the real request are treated as different fetches and the file is downloaded twice.

Why the font is late

A font is almost never late because it is large. It is late because of where it was referenced from: a font declared inside a stylesheet cannot be requested until that stylesheet has been fetched and parsed, and it is not requested even then unless the page actually uses that family. Two dependencies deep before the request is issued.

That is what preloading is for — and why preloading exactly one font is a fix while preloading five is a new problem. The table below covers the recurring failures rather than the happy path.

Font and image failures, and what they actually are
TriggerSymptomCauseResponse
Font referenced only from a late-discovered stylesheetText renders in the fallback for a long time, then swapsThe request cannot begin until the CSS has arrived, parsed, and matched an element using the familyPreload the one face used above the fold, with crossorigin, from the HTML (Resource Hints).
Four weights preloadedEverything on the page gets slower, including the contentPriority is relative; four high-priority fonts compete with the resources that render the pagePreload only the face the first screen renders in, and let the rest load normally.
Web font metrics differ from the fallbackThe whole article reflows when the font arrivesLine box heights depend on font metrics, so every line using the family is re-laid outAdd a metric-adjusted fallback @font-face, or accept optional and lose the font sometimes (Visual Stability).
One source image for every viewportA phone downloads far more pixels than it can displayNo candidate set, so the browser has exactly one optionsrcset with width descriptors, and a sizes value that matches the real rendered width (Responsive Images).
sizes copied from another componentThe markup looks correct and the wrong candidate is still chosensizes is a promise about layout width that nothing verifiesCheck the chosen candidate in devtools at several viewport widths; treat a mismatch as a bug (Media Queries Beyond Width).
Hero image marked loading="lazy"The main content element paints noticeably later than beforeLazy loading defers the request for the one image that should have been fetched firstKeep the initial viewport eager and mark the main image high priority (Loading: Why Content Arrives Late).
Icon font fails to loadRows of unrelated characters where icons should beThe glyphs live in private-use code points that no fallback font hasUse inline SVG with an accessible name, which fails to nothing rather than to noise (The Rules of ARIA).

How to build it

Most important first.

  • Serve the right dimensions. srcset with width descriptors plus an accurate sizes is the mechanism; getting sizes wrong is the most common way to keep the problem while adding the markup (Responsive Images).
  • Serve a modern format with a fallback, and encode per format rather than converting one export into three.
  • Always give images intrinsic dimensions — width and height attributes, or aspect-ratio — so their boxes exist before their bytes do.
  • Lazy-load below the fold and never in it. The initial viewport should be eager, and the main content element should carry fetchpriority="high".
  • Choose the font strategy deliberately rather than accepting a default, and write down why. The decision below is the lesson's core.
  • Subset to the glyphs the page can actually use, and split by unicode-range so a page of Latin text never downloads Cyrillic glyphs.
  • Preload the one font that renders above the fold — the body weight, usually — and only that one. Preloading four weights makes them compete with the content they are meant to render (Resource Hints).
  • Adjust the fallback's metrics so the swap does not reflow. This is what makes FOUT acceptable rather than merely faster (Visual Stability).
  • Use SVG or inline symbols for icons rather than an icon font, which sidesteps the whole font-loading problem for the case that needs it least.

Keyboard, focus, semantics, announcement

A required field on every lesson in this domain, not a section added when there is room.

  • Every image needs an alt decision, not an alt attribute copied around. Informative images get a description; decorative ones get alt="" so assistive technology skips them entirely (Semantics Are Behaviour).
  • Text hidden while a font loads is unavailable to sighted users and reads normally to a screen reader, which means the failure is invisible in an automated accessibility check and obvious to a person on a slow connection.
  • Icon fonts render as arbitrary characters in the accessibility tree when the font fails to load, and are announced as those characters. An inline SVG with a title, or a visually-hidden label, does not have this failure mode (The Rules of ARIA).
  • Font choices are legibility choices: sufficient size, sufficient contrast, and a fallback that is genuinely readable rather than merely present (Contrast, Colour and Motion).
  • Respect the user's font size preference. A layout that breaks when text is scaled up is an accessibility bug that a font optimisation can easily introduce (Responsive Typography).
  • Images that carry meaning must survive being unavailable — a broken image with a good alt is usable; a broken image with none is a gap.

What can go wrong

Failure modes
  • sizes that does not match the actual rendered width, so the browser confidently picks the wrong candidate. It is a promise you make to the browser and it is not checked.
  • Lazy-loading the hero, which is the single most common self-inflicted loading regression.
  • Preloading a font that turns out not to be used above the fold, spending priority on something invisible.
  • Preloading a font with the wrong crossorigin attribute, which fetches it twice — once for the preload and once for the real request.
  • font-display: optional on a brand font, which is correct for stability and means some users simply never see the brand font.
  • An aspect-ratio that disagrees with the image's real proportions, which trades a shift for permanent distortion or letterboxing.
  • Self-hosting fonts for privacy and losing the caching, compression and format negotiation the previous host was doing — a trade worth making deliberately.
What can arrive out of order
  • A font and the text it styles race, and the winner determines whether the user sees a swap, a flash of nothing, or neither (Visual Stability).
  • Multiple images race for bandwidth. Without explicit priority, a decorative image in the footer can be fetched ahead of the hero on a constrained connection.
  • A lazily-loaded image and a fast scroll race: scrolling faster than the request completes shows empty boxes exactly where the user is looking.
Security
  • A third-party font host sees a request for every page view: the user's address, user agent and referrer. Self-hosting is as much a privacy decision as a performance one.
  • Image decoders are large native attack surfaces, which is why browsers sandbox them. Serving user-uploaded images from your own origin means serving attacker-supplied bytes to that decoder (File Upload Security in Security).
  • An SVG is a document, not a bitmap. Inlined into the page it can carry script; loaded through img it cannot execute, which is the reason to prefer img for untrusted SVG (Sanitization and Trusted HTML).
  • Fonts and images from other origins need crossorigin handling to be usable in some contexts, and a font served without the right CORS response is silently not used.
Misreads
  • "Compression is the whole job." Sizing usually beats compression: an image displayed at a fraction of its natural width wastes most of its bytes regardless of how well they compressed.
  • "loading="lazy" everywhere is a best practice." It is a below-the-fold optimisation. In the initial viewport it is a regression.
  • "decoding="async" makes images load faster." It affects when decode blocks presentation, not when bytes arrive.
  • "FOUT is a bug." FOUT is a choice: readable text sooner in exchange for a visible change. FOIT is the other choice. Picking neither means accepting whatever the default is.
  • "Icon fonts are lighter than SVG." They ship a whole typeface to draw a handful of shapes, and they degrade into meaningless characters when they fail.
  • "We preloaded the fonts so they are fast." Preloading four weights spends priority on files most of which are not needed for the first screen.

Measuring it, and what changes in the field

How you would see this
  • The Network panel, sorted by transferred size and filtered to images — then compared against the rendered dimensions of each, which is where the oversized ones become obvious.
  • The element attributed to the largest contentful paint, which on most content pages is an image and is therefore the one worth optimising first (Loading: Why Content Arrives Late).
  • Layout-shift attribution, which names the element that moved and usually points at either an unsized image or a font swap (Visual Stability).
  • Decode time in the Performance panel, which is where a page that downloads quickly and still stutters gives itself away.
  • The Observability domain's image-performance treatment, for pricing this as a production signal rather than a page audit (Images: The Largest Bytes, Rarely the Largest Block in Observability).
Slow device, slow network, large data, old tab
  • On a high-density display the browser wants roughly twice the pixels per CSS pixel, so a candidate set without high-density entries is either blurry or oversized (The Viewport and Device Pixels).
  • On a slow connection the font strategy becomes visible: the difference between hiding text and swapping it is the difference between a blank article and a readable one.
  • On a memory-constrained device, decoded bitmaps rather than downloads are the constraint, and a long page of large images is a discarded tab.
  • For a returning visitor, fonts are usually cached and the whole problem disappears — which is precisely why it must be measured on a first visit (Browser HTTP Caching).
  • For a locale with a large character set, subsetting strategy changes completely: a Latin subset is small and a full CJK face is not, so unicode-range splitting matters far more (Internationalization).
What this costs
  • Modern image formats are smaller and cost encode time, tooling and a fallback chain. The picture element makes the fallback declarative and makes the markup longer.
  • Subsetting reduces bytes and risks missing glyphs — a name with an unexpected diacritic renders in the fallback, mid-sentence.
  • font-display: optional gives stability at the cost of sometimes never showing the font; swap shows it and guarantees a swap. There is no setting that gives both.
  • Self-hosting fonts improves privacy and control and gives up whatever the third-party host was doing well, including per-browser format negotiation.
  • Preloading a font moves it earlier at the expense of everything else in flight, which is exactly the right trade for one file and the wrong one for four.

Where this applies

Frontend advice ages badly and fragments across engines. These labels say what each claim is specific to, and where a different browser, device or framework would differ.

  • GENERALThe mechanisms — candidate selection from srcset and sizes, reserved boxes from intrinsic dimensions, and the FOIT/FOUT choice expressed by font-display — are specified behaviour and are implemented across engines.
  • BROWSER-SPECIFICFormat support and the exact heuristics for lazy-loading distance, priority and decode scheduling differ between browsers and change between versions, so a picture fallback chain is required rather than optional and lazy thresholds should never be relied on precisely.
  • NETWORK-SPECIFICOn a fast connection nearly all of this is invisible because everything arrives before first paint; on a constrained mobile connection the same page is dominated by image bytes and by whichever font strategy was chosen.
  • DEVICE-SPECIFICDevice pixel ratio changes which candidate is correct, and available memory changes whether decoded bitmap size or download size is the binding constraint — so the right answer on a high-density phone differs from the one on a standard-density laptop.

Where the depth lives

This domain teaches the browser-side mechanism and hands the rest off.

Domains that do not exist yet
  • Software Design — a font strategy is a product decision about what the user should see while waiting, and it belongs in the design system rather than in whichever stylesheet happened to declare the family.
OS & Networkingcdn-networking