What you'll learn: By the end of this lesson you will be able to choose an image format for a given kind of picture, write
srcset,sizesand<picture>markup that lets the browser pick the right file, decide which images to lazy-load and which never to, reserve space so nothing jumps, load a web font with a sensiblefont-displayvalue, preload it correctly, shrink it by subsetting, and reduce the shift when it swaps in.
Think of a photo lab. A poster-sized print for a wallet wastes paper and time; a wallet print on a wall looks blurry. A page makes both mistakes thousands of times: a 4,000-pixel photo in a 300-pixel slot, or a small one in a large slot. Fonts are small files that decide how every word looks, and the browser often learns about them late.
Images and fonts arrive after the HTML yet decide what the page looks like. Both come down to four questions: which format, what size, when to load, and what to show in the meantime. The running example is a product listing page: a hero image, 24 thumbnails and headings in a brand font.
The core idea
- The browser can only choose among files you provide. Offer a few well-made candidates and describe the slot; the browser picks.
- Timing matters as much as bytes. A file discovered late, or deferred when it should not be, hurts as much as one that is too big.
- Say what happens before the file arrives: reserve an image's space; choose which fallback text a font shows, and for how long.
The Largest Contentful Paint image is covered in LCP optimization, and layout shift in CLS and layout stability. This lesson covers the everyday engineering around the files.
Images: format first
MDN's image type guide gives the working rules. For raster images it recommends preferring WebP or AVIF, which "generally provide better compression than PNG, JPEG, and GIF", with a PNG or JPEG fallback through <picture> for browsers that lack support. Photographs tolerate lossy compression; screenshots, diagrams, logos and line art want lossless. For graphics that must stay sharp at any size, MDN calls SVG "ideal for user interface elements, icons, diagrams, etc.", and "not generally useful" for photographs.
Two details are easy to miss. MDN reports lossy WebP files are on average 25 to 35 percent smaller than JPEGs of visually similar quality (a typical figure, not a guarantee), and AVIF "does not support progressive rendering, so files must be fully downloaded before they can be displayed". Check browser-support tables, which change; the fallback pattern below does not.
Images: right size for the slot
A file that is far larger than the slot wastes bytes; one that is smaller looks soft. HTML solves this by letting you list candidates and describe the slot.
Figure 1. How one image reaches the page
How to read this: the work you control happens once, on the left. The choice on the right happens in every browser. You never force a file; you make the right ones available and describe the slot.
srcset with x descriptors: same size, sharper screens
When the image is always shown at one CSS size, list it at different pixel densities. MDN's example gives a 1x, a 1.5x and a 2x file, and a device with two device pixels per CSS pixel or more loads the 2x file. No sizes is needed.
srcset with w descriptors and sizes: different sizes in different layouts
When the displayed size changes with the layout, give each candidate its intrinsic width (480w: "note that this uses the w unit, not px"), and tell the browser how wide the image will be with sizes. MDN describes sizes as "a set of media conditions ... and indicates what image size would be best to choose". The browser takes the first media condition that is true, uses that slot size, and picks a candidate; with no exact match it chooses the first image bigger than the slot. It also weighs pixel density, zoom and possibly network speed, so you cannot force a choice.
The HTML Standard adds two rules worth knowing. When a sizes value yields no usable size, the result is 100vw: so an image with w descriptors and no sizes is treated as if it fills the viewport width. And when an element has sizes, every candidate must use a width descriptor.
Illustration 1: the minimal example
<img
src="/img/boots-800.jpg"
srcset="/img/boots-400.jpg 400w, /img/boots-800.jpg 800w, /img/boots-1600.jpg 1600w"
sizes="(min-width: 60em) 33vw, 100vw"
width="800" height="600"
alt="A pair of leather boots on a wooden floor">
On a viewport at least 60em wide the image fills about a third of the width, otherwise all of it. On a 1,200 pixel viewport (illustrative) the slot is about 400 CSS pixels, so a screen with two device pixels per CSS pixel wants about 800 pixels and the 800w file fits best; the exact pick varies by browser. Browsers that ignore srcset "will just ignore them" and load src.
Art direction and formats with <picture>
<picture> wraps several <source> elements and a final <img>. The spec says the browser chooses "the first source element for which the media query in the media attribute matches", and for formats "the first source that has a type attribute with a supported MIME type". MDN says to use media only for art direction (a different crop on a narrow screen). The final <img> is mandatory: "otherwise no images will appear".
<picture>
<source type="image/avif" srcset="/img/boots-400.avif 400w, /img/boots-800.avif 800w" sizes="(min-width: 60em) 33vw, 100vw">
<source type="image/webp" srcset="/img/boots-400.webp 400w, /img/boots-800.webp 800w" sizes="(min-width: 60em) 33vw, 100vw">
<img src="/img/boots-800.jpg" width="800" height="600" alt="A pair of leather boots on a wooden floor">
</picture>
Figure 2. Choosing the markup for an image
and a sizes attribute"] E --> F{"Different crop per screen?"} F -->|"yes"| G["picture with media"] F -->|"no"| H{"Need format fallbacks?"} H -->|"yes"| I["picture with type, img last"] H -->|"no"| J["img alone"]
How to read this: most photos end at w descriptors plus sizes; <picture> is for crops and format fallbacks, not for every image.
Images: when to load, and what space to keep
Reserve the space. MDN says that including width and height "enables the aspect ratio of the image to be calculated by the browser prior to the image being loaded", which reserves the space and reduces or prevents layout shift. It stresses that this matters most for lazy-loaded images, whose unloaded size would otherwise be zero.
Lazy-load below the fold. loading="lazy" "defers loading the image until it reaches a calculated distance from the viewport, as defined by the browser"; eager is the default. The rule from web.dev's LCP guide is blunt: "Never lazy-load your LCP image", and mark a likely LCP image fetchpriority="high" but only one or two images, because "setting a high priority on more than one or two images makes priority setting unhelpful". The HTML Standard defines fetchpriority values as high, low and auto (the default), relative to other resources of the same type.
Alt text. MDN calls alt "mandatory": screen readers read it.
Fonts: the same four questions
A web font is found through a @font-face rule, usually after the stylesheet is fetched. Until it arrives the browser shows something or nothing; that choice is font-display.
Figure 3. The font display timeline
How to read this: each font-display value sets two timers. In the block period text is invisible; in the swap period a fallback shows and the real font replaces it on arrival.
| Value | Block period | Swap period | What the reader sees |
|---|---|---|---|
block |
short (3s recommended in most cases, per the CSS Fonts draft) | infinite | invisible text first, then the web font whenever it arrives |
swap |
extremely small (100 ms or less recommended) | infinite | fallback at once, then a swap |
fallback |
extremely small | short (3s recommended) | fallback, swap only if the font is quick |
optional |
extremely small | none | fallback unless the font was ready almost immediately |
auto |
user-agent defined | user-agent defined | depends on the browser |
These are MDN's definitions, with durations from the CSS Fonts Level 4 text. Web.dev says optional suits performance, swap quick text with a web font, block text always in the web font; swap "delays text render the least" among options that can shift layout.
Make the font small and early
- Use WOFF2. Web.dev quotes the 2022 Web Almanac advice to "use only WOFF2", and says it "compresses 30% better than WOFF".
- Subset and use
unicode-range. "Removing unused glyphs can significantly reduce the filesize of a font" (web.dev). MDN adds that withunicode-range, "if the page doesn't use any character in this range, the font is not downloaded; if it uses at least one, the whole font is downloaded." - Fewer fonts: "The fastest font to deliver is a font that isn't requested in the first place."
- Preload the critical one, with a detail that trips people up:
<link rel="preload" href="/fonts/brand-regular.woff2" as="font" type="font/woff2" crossorigin>
MDN says fonts need the crossorigin attribute "even when the fetch is not cross-origin", because they are fetched in anonymous CORS mode. Web.dev notes preloading "takes browser resources from other loads", so preload the one or two fonts that appear in the first screen, not the whole family.
Figure 4. Why a preload helps a font
How to read this: without the preload line the font request starts only after the CSS is read, so the font arrives later.
Reduce the jump when the font swaps
When a fallback font is replaced, text can reflow, which counts as layout shift. MDN's size-adjust page describes it as "a multiplier for glyph outlines and metrics" and says it "can help when overriding the metrics of a fallback font to better match those of a primary web font"; web.dev suggests it to reduce the CLS from fallback-to-web-font swaps. MDN lists it as Baseline widely available since September 2023. Measure before and after.
Trade-offs
| Choice | Gain | Cost | Watch for |
|---|---|---|---|
| AVIF and WebP with a JPEG fallback | Smaller files | Several files per image, an encoding step | A pipeline that forgets the fallback |
loading="lazy" |
Fewer bytes at load | Images arrive late | A lazy first-screen image |
font-display: swap |
Text visible immediately | A visible swap | Layout shift on swap |
font-display: optional |
No swap | The brand font may not appear on the first view | Brand teams who expect it everywhere |
| Preloading a font | Earlier fetch | Competes with other early resources | Missing crossorigin; too many fonts |
Illustration 2: the realistic example, a product listing
The page has a hero, 24 thumbnails (4 columns on a wide screen, 2 on a phone) and a brand heading font. A sound setup: the hero has srcset, sizes, dimensions and fetchpriority="high", and no lazy loading; thumbnails use sizes="(min-width: 60em) 25vw, 50vw", dimensions, and loading="lazy" below the first row; the font is a subset WOFF2 with font-display: swap, preloaded once.
Illustration 3: when sizes is missing
A developer adds srcset="a-400.jpg 400w, a-1600.jpg 1600w" to every thumbnail and forgets sizes. The HTML Standard's default is 100vw, so the browser treats each thumbnail as full-width and may pick the 1,600-pixel file for a 300-pixel slot. Nothing errors; the page is simply heavier. The fix is one attribute and a review rule.
In practice: the listing page that got heavier after a redesign
This is a realistic scenario, not a case history. It is a composite built from common production patterns. It describes no specific company or product, and every number in it is an illustrative assumption.
The situation
A redesign adds larger thumbnails and a new brand font. After release, page weight on phones rises and headings flash in another font before settling. A CMS stores each uploaded photo and serves the original as is. Three engineers and a designer own the page.
Starting architecture
Figure 5. Originals served directly, font discovered late
How to read this: every thumbnail downloads the original, and the font request starts late.
Options considered
| Option | What it fixes | What it costs | Risk |
|---|---|---|---|
Resize and encode at upload or in an image service, add srcset and sizes |
Bytes per slot | A pipeline step | Wrong sizes values |
| Lazy-load all images | Fewer early bytes | Hero arrives late | Lazy-loading the hero |
The decision
| Field | Decision record |
|---|---|
| Context | Originals are served to every slot and the new font is found late; the page is heavier and jumps. |
| Decision | A handful of widths in AVIF and WebP with a JPEG fallback; srcset, sizes, width and height everywhere; hero eager with high fetch priority; thumbnails below the first row lazy; one subset WOFF2 preloaded with crossorigin; font-display: swap. |
| Consequences, good | Fewer bytes per page; stable layout; text visible at once. |
| Consequences, bad | An image pipeline to maintain; a visible swap on slow connections. |
| Revisit when | Field CLS or LCP regress, or the brand allows optional. |
Rollout and verification
- Inspect the choice: in developer tools, compare the file each slot downloaded with its slot size.
- Throttle the network: is text visible, and does it jump when the font arrives?
- Compare field metrics (LCP, CLS) before and after.
- Add review rules: every
wimage hassizes; every image haswidth,heightandalt; no lazy hero.
The 2 a.m. failure
The font host has an incident and the heading font never arrives. With swap the headings stay in the fallback font and the page is usable. Had someone changed it to block to hide the flash, headings would be invisible for the block period, three seconds under the CSS Fonts recommendation, on every page view.
First three checks: (1) the font request's status; (2) the font-display value; (3) whether text is invisible or in the fallback. Mitigation: serve the font from the main origin. Follow-up: keep swap or optional, self-host where practical (web.dev says this helps most with a CDN and HTTP/2), and add a synthetic check with the font blocked.
Architect's notes
- Make the pipeline the default: resizing, encoding and
srcsetgeneration belong in a build step or service, not in each author's hands. - Lint the markup. Missing
sizes, missing dimensions and a lazy hero are all catchable. - Pick
font-displaydeliberately: it decides what readers see on a slow connection. - Preload sparingly: one or two fonts and the hero.
- Measure on a throttled device; office hardware hides these problems.
Common mistakes
- Lazy-loading the hero or any first-screen image.
- Using
wdescriptors withoutsizes, so the browser assumes the image fills the viewport. - Leaving out
widthandheight, so the page shifts as images arrive. - Forgetting
crossoriginon a font preload, even from the same origin. - Preloading several formats of one resource: MDN warns all would download.
- Shipping the whole font family when the page uses one weight.
Key takeaways
- Choose the format by content: WebP or AVIF with a fallback for photos, SVG for icons and diagrams.
srcsetoffers candidates,sizesdescribes the slot, and the browser chooses; withwdescriptors and nosizesthe slot is assumed to be100vw.- Always give
width,heightandalt; lazy-load below the first screen only and never the LCP image. font-displaysets how long text is invisible and how long a fallback may be swapped;swapis the quick-text choice,optionalthe no-jump choice.- Use WOFF2, subset, and preload only the first screen's one or two fonts, with
crossorigin. - Reduce swap shift by matching fallback metrics, for instance with
size-adjust.
Practice exercises
Exercise 1 — Starter (30 min): Write srcset and sizes for one image on a page of your own, compute its slot width at two viewport widths, and say which candidate you expect.
Exercise 2 — Intermediate (2 hours): Throttle the network and record what you see for swap, optional and block on a page with a web font.
Exercise 3 — Advanced (half day): Write a script that scans a page's HTML for images with w descriptors but no sizes, images without width and height, and a lazy first image. State what it cannot see.
Related reading
In this course:
- LCP optimization, discovering and prioritising the hero image.
- CLS and layout stability, the layout-shift side of dimensions and font swaps.
- HTTP caching, caching the files once you have made them.
Official documentation
- MDN: Responsive images and HTML Standard: Images,
srcset,sizesand<picture>. - MDN: The img element,
width,height,loading,fetchpriorityanddecoding. - MDN: Image file type guide, formats and when to use each.
- MDN: font-display and CSS Fonts Module Level 4, the display periods.
- web.dev: Best practices for fonts, WOFF2, subsetting, preloading and
size-adjust. - MDN: rel=preload, the
crossoriginrequirement for fonts.