Skip to content

    Frontend System Design

    Image and Font Optimization

    Learn how to pick image formats, serve responsive images with srcset and sizes, lazy-load safely, and load web fonts with font-display, preload and subsetting.

    By Published 12 min read

    • frontend-system-design
    • images
    • web-fonts
    • responsive-images
    • performance
    Share

    Standard lessonbeginnerFrontend System Design

    Before this: Loading Performance and Core Web Vitals

    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, sizes and <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 sensible font-display value, 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

    1. The browser can only choose among files you provide. Offer a few well-made candidates and describe the slot; the browser picks.
    2. 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.
    3. 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

    An image pipeline: one source file becomes several sizes and formats, and the browser picks oneSource image to Image build step; Image build step to Variants (emit); Variants to img or picture (listed); img or picture to Browser (chooses)BUILD TIME OR IMAGE CDNSource image: one large originalSource imageone largeoriginalImage build step: resize, encodeImage buildstepresize, encodeVariants: widths x formatsVariantswidths x formatsimg or picture: srcset, sizes, typeimg or picturesrcset, sizes,typeBrowser: picks one fileBrowserpicks one fileemitlistedchooses

    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

    flowchart TD accTitle: Choosing markup for an image accDescr: A decision tree. A vector graphic uses SVG. A raster image shown at a fixed size uses srcset with x descriptors. A raster image whose size changes with layout uses srcset with w descriptors and sizes. A different crop per screen uses picture with media, and format fallbacks use picture with type. A["What kind of image?"] -->|"icon, logo, diagram"| B["SVG"] A -->|"photo or raster"| C{"Same size in every layout?"} C -->|"yes"| D["srcset with x descriptors"] C -->|"no"| E["srcset with w descriptors
    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

    stateDiagram-v2 accTitle: Font display timeline accDescr: When a page first needs a web font, the browser enters the block period, where text is invisible using a fallback. If the font loads, it is used. After the block period comes the swap period, where fallback text is shown and swaps to the web font when it loads. After the swap period the load is treated as failed and the fallback stays. [*] --> Block: font needed Block --> WebFont: font loads Block --> Swap: block period ends Swap --> WebFont: font loads Swap --> Failure: swap period ends Failure --> [*]: fallback stays WebFont --> [*]

    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 with unicode-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

    sequenceDiagram accTitle: Font fetch with and without preload accDescr: Without preload the browser fetches the stylesheet first, then discovers the font rule, then fetches the font. With a preload link in the document head the font request starts in parallel with the stylesheet. participant B as Browser participant S as Server B->>S: GET HTML S-->>B: HTML with preload link and stylesheet link par Stylesheet B->>S: GET styles.css S-->>B: CSS with @font-face and Preloaded font B->>S: GET brand-regular.woff2 S-->>B: font file end Note over B: text that needs the font can use it as soon as layout needs it

    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

    NOTE

    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

    Starting architecture: full-size originals in every slot, the brand font found only through the stylesheetCMS to Listing page (image URLs); Listing page to Originals (24 requests); Listing page to Brand font (late request)CMS: stores originalsCMSstores originalsListing page: img with one srcListing pageimg with one srcOriginals: same file, every slotOriginalssame file, everyslotBrand font: found via the CSSBrand fontfound via the CSSimage URLs24 requestslate request

    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

    1. Inspect the choice: in developer tools, compare the file each slot downloaded with its slot size.
    2. Throttle the network: is text visible, and does it jump when the font arrives?
    3. Compare field metrics (LCP, CLS) before and after.
    4. Add review rules: every w image has sizes; every image has width, height and alt; 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 srcset generation 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-display deliberately: 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 w descriptors without sizes, so the browser assumes the image fills the viewport.
    • Leaving out width and height, so the page shifts as images arrive.
    • Forgetting crossorigin on 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.
    • srcset offers candidates, sizes describes the slot, and the browser chooses; with w descriptors and no sizes the slot is assumed to be 100vw.
    • Always give width, height and alt; lazy-load below the first screen only and never the LCP image.
    • font-display sets how long text is invisible and how long a fallback may be swapped; swap is the quick-text choice, optional the 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.

    In this course:

    Official documentation

    Ask about this article

    Get answers grounded in this post. AI-generated — based on this article, and may be imperfect.

    Scaled AI Weekly

    Enjoyed this? Get more like it every Monday.

    Real architecture decisions, LLMOps patterns that survive production, and engineering leadership advice — from 12+ years of building at enterprise scale. Free. No spam. Unsubscribe anytime.

    Get new articles by email

    Free. We email a confirmation link first, and you can unsubscribe from any email.

    Join engineers building production AI systems

    Comments