cf.completefrontend
My progress
FUNDAMENTALS · MODULE 8

Images and media

Sending each user an image of the right size and format, reserving its space, and loading it at the right moment: images are usually the largest share of a page's bytes and its LCP element.

Why images need their own plan

Picture a shoe shop. The designer exports the hero photo of this season's running shoe at 4000 by 2667 pixels, saves it as a high-quality JPEG of 2.4 MB, and a developer drops it into the page with a plain img tag. On the designer's 27-inch screen it looks perfect.

Now open it on a mid-range phone on 4G, 412 CSS pixels wide. Even at a device pixel ratio of 2.625 it shows only about 1080 pixels across. The phone downloads 2.4 MB, decodes about 43 MB of pixels, and throws away over 90 percent of them to fit.

While all of that happens, three things go wrong at once. The text under the photo jumps down when the image finally gets its height. The largest thing on screen, the hero, appears seconds late. And the user pays for megabytes of mobile data they never see.

Three failures, three metrics

Each of those failures maps to a number that Google and your own monitoring track. This is why images get their own plan in a frontend system design: they touch more user-facing metrics than any other kind of file.

  • Too many bytes. The image is bigger than the screen needs, or in an old format. This slows LCP, Largest Contentful Paint, the time until the biggest element on the first screen is drawn, which is usually an image.
  • No reserved space. The browser does not know the image's height until the file arrives, so it lays out a zero-height box and moves everything later. That movement is CLS, Cumulative Layout Shift.
  • Wrong timing. Images far below the fold compete with the hero for bandwidth, or the hero is lazy-loaded by mistake and starts late.
A bar chart of one hero image in four versions, drawn to scale. The 4000 pixel original JPEG is 2,400 KB. Resized to 1200 pixels as JPEG it is about 190 KB. As WebP at 1200 pixels, about 140 KB. As AVIF at 1200 pixels, about 95 KB.one hero image on a phone, bytes to scaleoriginal 4000 px JPEG2,400 KBresized to 1200 px, JPEGabout 190 KB1200 px, WebPabout 140 KB1200 px, AVIFabout 95 KB
Resizing does most of the work; a modern format takes the next big step. The format numbers are typical for one photo, not a promise: measure your own images.

How big the problem is

The HTTP Archive's Web Almanac 2025 measured about 911 KB of images on the median mobile home page. Per byte, images cost less than JavaScript, because decoding mostly happens off the main thread. But there are a lot of them, and they sit right on the path to LCP.

The good news: image work is mostly mechanical. Five moves fix most pages: resize, pick a modern format, reserve space, load in the right order, and show something while waiting. This lesson covers those five in depth, plus video.

Where this lesson fits

Core Web Vitals explains how LCP and CLS are measured. Loading strategies covers fetchpriority, preload, and loading="lazy" for every resource. Estimation for frontend has the formula for decoded image memory. This lesson focuses on the image itself: which file, which size, which format, and how it arrives.

Check your understanding · Exercise 1

A product page shows a 2.4 MB hero JPEG with no width or height. Which problems does that cause on a phone? Select all that apply.

The mental model: measure the frame, then order the print

Think of a picture frame on a wall. You would not order a poster-sized print for a postcard frame. You measure the frame, order the print that fills it, and hang the empty frame first, so nothing on the wall moves when the print arrives.

That is the whole idea of image optimization. The frame is the box the image takes in the layout. The print is the file the browser downloads. Your job is to make the print match the frame, in the cheapest material that still looks right, and to hang the frame early.

Four questions for every image

Every image on a page answers four questions, whether you answer them on purpose or not. A plain img with one large JPEG answers all four badly: one size for every screen, an old format, unknown space, and default timing.

The right column matters: for size, the browser makes the final call, because only it knows the screen, the zoom level, and sometimes the network.
QuestionThe toolWho decides
How many pixels?srcset with width descriptors, plus sizesYou list the candidates; the browser picks one
Which format?picture with type, or the CDN reading the Accept headerYou or the image CDN
How much space?width and height attributes, or CSS aspect-ratioYou, in the markup or CSS
When does it load, and what shows meanwhile?fetchpriority, loading, a placeholderYou, based on where it sits on the page

The pipeline from upload to pixels

It also helps to see where each decision lives. An image passes several stops before it reaches the screen.

  1. The original is stored once, at full size. Nobody downloads it directly.
  2. An image CDN or build step makes variants: several widths and formats, each with its own cached URL.
  3. The HTML describes the variants: URLs with widths, the layout size, the aspect ratio, and the priority.
  4. The browser adds what only it knows (viewport, DPR, supported formats) and requests exactly one variant.
  5. Decode and paint. The file becomes a bitmap of raw pixels, drawn into the box reserved for it.
A stack of five stops an image passes. The original is stored once at full size and never sent. An image CDN or build step makes variants, deciding widths, format, and quality. The HTML markup describes the variants with srcset, sizes, width, and height. The browser, which knows the screen and DPR, decides which one file to fetch. Finally the file is decoded into a bitmap and painted into its box, using width times height times 4 bytes of memory.Server sidePage and browserOriginalstored once, full sizenothing yet: never sentImage CDN or buildmakes variantswidths, format, qualityHTML markupdescribes the variantssrcset, sizes, width, heightBrowserknows screen and DPRwhich one file to fetchDecode and paintbitmap into the boxmemory: w x h x 4 bytes
Each stop can only shrink what the stop above gave it. The browser makes the final size choice, but only from the variants you made and described in srcset.

Notice who does the hard work. You never ask JavaScript to measure the screen and choose a URL: that runs too late and hides the image from the browser's preload scanner. You describe the options in HTML and let the browser choose while it is still parsing.

Responsive images: srcset and sizes

The first question is how many pixels to send. The answer depends on two things the server cannot know: how wide the image's box will be, and how many device pixels the screen packs into each CSS pixel. HTML lets you describe several files and lets the browser choose.

CSS pixels and device pixels

Layout uses CSS pixels. A phone whose layout is 412 CSS pixels wide may have a screen that is 1080 physical pixels wide. The ratio between the two is the device pixel ratio, or DPR, which JavaScript can read as window.devicePixelRatio.

On that phone the DPR is 2.625. Most laptops are 1 or 2, most phones 2 to 3.

An image looks sharp with at least one image pixel per device pixel. So a 400 CSS pixel box on a DPR 2 screen wants an 800 pixel file. On a DPR 1 screen that file wastes three quarters of its pixels, because pixel count grows with the square of the width.

Fixed-size images: density descriptors

If an image is always drawn at the same CSS size, such as a 120 pixel logo or a 48 pixel avatar, list one file per density with an x descriptor. The browser picks the one that matches the screen's DPR.

html
<img
  src="/logo-120.png"
  srcset="/logo-120.png 1x, /logo-240.png 2x, /logo-360.png 3x"
  width="120" height="40"
  alt="Stride Shoes">
Density descriptors: the CSS size is fixed, only the screen density changes.

Fluid images: width descriptors and sizes

Most content images are fluid: full width on a phone, half width on a tablet, a fixed column on a desktop. For these, describe each file by its real pixel width with a w descriptor, and tell the browser how wide the box will be with sizes.

html
<img
  src="/shoe-800.jpg"
  srcset="/shoe-480.jpg 480w, /shoe-800.jpg 800w,
          /shoe-1200.jpg 1200w, /shoe-1600.jpg 1600w,
          /shoe-2400.jpg 2400w"
  sizes="(min-width: 1200px) 1200px, 100vw"
  width="1200" height="800"
  alt="Blue running shoe, side view">
Width descriptors: each file states its own width; sizes states the box width for each layout.

sizes is a list of media conditions, each followed by a length, read left to right. The first condition that matches wins, and the last entry has no condition: it is the fallback. Lengths can be px, vw, em, or calc(), but not percentages, because the browser has not done layout yet and has nothing to take a percentage of.

Why say the width at all? Because the browser picks the image while reading raw HTML, before CSS and layout. sizes is your promise about a layout it has not computed yet.

The selection math, step by step

  1. Find the slot width. Evaluate sizes against the viewport. On a 412 pixel phone, (min-width: 1200px) is false, so the slot is 100vw, which is 412 CSS pixels.
  2. Find the target. Multiply by the DPR: 412 times 2.625 is about 1082 device pixels.
  3. Compute each candidate's density. Divide the candidate's width by the slot width: 800 / 412 is 1.94x, 1200 / 412 is 2.91x.
  4. Pick the smallest candidate that is dense enough. 800w gives 1.94x, which is below 2.625, so it would look a little soft. 1200w gives 2.91x, the first one at or above the DPR. The browser fetches shoe-1200.jpg.
A flowchart of srcset selection on a phone 412 CSS pixels wide at DPR 2.625. Step one: evaluate sizes; the condition min-width 1200px is false. Step two: the slot is 100vw, 412 CSS pixels. Step three: the target is 412 times 2.625, about 1082 device pixels. Then the candidates are tried from small to large: 800w gives 1.94x, below 2.625, so no. 1200w gives 2.91x, enough, so yes. The browser fetches shoe-1200.jpg.Evaluate sizes(min-width: 1200px)? noSlot = 100vw412 CSS pxTarget = slot x DPR412 x 2.625 = 1082 px800w: 1.94xbelow 2.625no1200w: 2.91xreaches 2.625yesFetch shoe-1200.jpgone requestRule: smallest file whose width / slot reaches the DPR
The browser never looks at your CSS here: it trusts sizes. Density is candidate width divided by slot width, and the first candidate that reaches the DPR wins.

The same markup gives different answers elsewhere. A 1440 pixel laptop at DPR 1 matches the first condition, so the slot is 1200 pixels and the target is 1200: it fetches 1200w. The same laptop at DPR 2 wants 2400 device pixels and fetches 2400w. A 360 pixel phone at DPR 3 wants 1080 and fetches 1200w.

This is a model, not a law: the spec lets the browser pick any candidate. Current Chrome follows it closely, but reuses a larger candidate already in its memory cache instead of fetching a smaller one. Browsers also tend to keep the larger file when the window shrinks, and the spec allows picking smaller on a slow connection or in data-saving mode. Try the model in the lab below.

DPR:
Image:
Viewport 412 CSS pixels at DPR 2.625. sizes is 100vw, so the slot is 412 CSS pixels and the target is 1082 device pixels. From candidates 320w to 2400w, the browser picks 1200w, about 94 KB as AVIF.viewport: 412 CSS pxsizes="100vw"slot 412 CSS px x DPR 2.625 = target 1082 px320w7 KB0.78x480w15 KB1.17x800w42 KB1.94x1200w94 KB2.91x1600w167 KB3.88x2400w375 KB5.83xpickedmatches the drawn size: 412 CSS px
The model: slot width from sizes, times DPR, then the smallest candidate that reaches it. Sizes are rough AVIF estimates for a 3:2 photo. Try the grid tile with sizes missing on a wide screen.

Slot 412 CSS px, target 1082 device px, picked 1200w (about 94 KB).

Choosing the candidate widths

Too few widths, and some screens get a file far bigger than they need. Too many, and the CDN cache splits across variants that each get fewer hits. A practical rule: 4 to 6 widths from about 320 to 2400, each step roughly 20 to 30 percent more bytes than the last.

Many teams also cap large photos at 2x. A 3x file has 2.25 times the pixels of a 2x file, and at arm's length most people cannot tell them apart. You cap by leaving the biggest widths out of srcset.

sizes="auto" for lazy images

For lazy images there is a newer shortcut: sizes="auto". By the time a lazy image loads, layout is done, so the browser can measure the real box. It only applies with loading="lazy", and the image still needs width and height so the box has a size before it loads.

Chrome has supported it since version 126 (2024), Firefox since 150, and Safari since 27 (September 2026). Older browsers skip the auto entry, so write it first and keep your usual value after it as a fallback: sizes="auto, (min-width: 1024px) 25vw, 50vw".

Check your understanding · Exercise 2

Candidates are 400w, 800w, 1200w, 1600w. sizes is (min-width: 900px) 33vw, 100vw. A laptop has a 1500 pixel wide viewport at DPR 2. Which file does the model pick? Pick one.

The picture element: art direction and format fallback

srcset assumes every candidate is the same picture at a different resolution, and lets the browser choose freely. When you need a different crop on a phone, or a new format with a safe fallback, use picture.

How picture works

A picture holds zero or more source elements and exactly one img. The browser walks the sources from top to bottom and takes the first one whose media matches and whose type it supports. If none match, it uses the img. The chosen source only supplies the URL list: the img is still the element that is laid out, styled, and read by screen readers, so alt, width, height, loading, and fetchpriority all go on the img.

html
<picture>
  <source type="image/avif"
    srcset="/shoe-800.avif 800w, /shoe-1600.avif 1600w"
    sizes="(min-width: 1200px) 1200px, 100vw">
  <source type="image/webp"
    srcset="/shoe-800.webp 800w, /shoe-1600.webp 1600w"
    sizes="(min-width: 1200px) 1200px, 100vw">
  <img src="/shoe-800.jpg"
    srcset="/shoe-800.jpg 800w, /shoe-1600.jpg 1600w"
    sizes="(min-width: 1200px) 1200px, 100vw"
    width="1200" height="800" alt="Blue running shoe, side view">
</picture>
Format fallback: AVIF if supported, then WebP, then JPEG. Each source still uses srcset and sizes.
A picture element, one line per row, with notes. The picture tag is a wrapper with no box of its own. The first source, type image/avif, is used if the browser supports AVIF. The second source is WebP, the next choice. The img with src shoe.jpg and its srcset is the JPEG fallback and also the element that is laid out. Its width, height, alt, and loading attributes are where every attribute lives.<picture>a wrapper, no box of its own <source type="image/avif" …>first choice if AVIF works <source type="image/webp" …>next: WebP <img src="shoe.jpg" srcset=…JPEG fallback, laid out width height alt loading>every attribute lives here</picture>
Sources only supply URLs. The img is what gets laid out, styled, and read aloud, so alt, dimensions, loading, and fetchpriority all belong on it.

Two different jobs

  • Format fallback uses type. Every source shows the same picture; they differ only in encoding. Order matters: put the best format first, because the first supported one wins even if a later one would be smaller.
  • Art direction uses media. The sources show different pictures: a wide landscape banner on desktop, a tight square crop of the shoe on a phone where the landscape version would make the shoe tiny. Here you are overriding the browser, which is the point.

You can combine both, but sources multiply: two crops times three formats is six. An image CDN that picks the format from the request header (covered later) removes the format half.

html
<picture>
  <source media="(max-width: 600px)"
    srcset="/sale-square-600.avif 600w, /sale-square-1200.avif 1200w"
    width="600" height="600">
  <img src="/sale-wide-1600.avif"
    srcset="/sale-wide-1600.avif 1600w, /sale-wide-2400.avif 2400w"
    sizes="100vw" width="1600" height="600" alt="Autumn sale: 30 percent off trail shoes">
</picture>
Art direction: a square crop on phones and a wide banner elsewhere. Each source carries its own width and height, so the browser reserves the right box for each crop.

When the crops have different aspect ratios, put width and height on each source as well as on the img. Chrome 90, Firefox 108, Safari 15, and later versions use them to reserve the right space for whichever source wins; without them, the phone reserves the banner's shape and then shifts.

Decision The hero is a wide banner. On phones the product inside it becomes tiny. How do you serve a better crop?

One image, cropped with CSS

Serve the wide file everywhere.

On small screens, use object-fit: cover with object-position to show only the middle.

Pros

  • One set of files and one srcset.
  • No extra markup.

Cons

  • The phone still downloads the pixels it hides.
  • CSS percentages, not a person, decide the crop.

picture with media sources

Export a separate crop per breakpoint.

List them as source elements with media, width, and height.

Pros

  • Each screen downloads only what it shows.
  • A designer controls each crop exactly.

Cons

  • More files to produce and keep in sync.
  • Markup grows quickly when combined with format fallbacks.

CDN crop by URL

Keep one original.

Ask the image CDN for a crop in the URL, with a focal point or automatic subject detection, inside picture sources.

Pros

  • No manual exports; new crops are a URL change.
  • Works for user uploads, where no designer is involved.

Cons

  • Automatic subject detection can cut off the wrong thing.
  • Ties your markup to one vendor's URL syntax.

What we pick: For a few campaign banners, use picture with media and designer-made crops. For thousands of product or user photos, use CDN crops with a stored focal point. Use the CSS crop only when the hidden area is small.

Formats: AVIF, WebP, JPEG, and the rest

The second question is which format. Right-sizing removes pixels you do not need; a better format stores the rest in fewer bytes. The two savings multiply.

Lossy, lossless, raster, vector

  • Lossy formats throw away detail the eye is unlikely to notice, and you choose how much with a quality setting. Right for photos.
  • Lossless formats keep every pixel exactly. Right for screenshots, diagrams, and anything with sharp edges and flat color, where lossy artifacts show as smudges around text.
  • Raster formats store a grid of pixels. Vector formats, which on the web means SVG, store shapes and scale to any size with no extra bytes. Logos and icons should almost always be SVG.

The formats you will actually use

Savings are typical ranges, not promises. Google's WebP study measured 25 to 34 percent smaller files than JPEG at similar quality; AVIF results vary widely by image and encoder settings.
FormatGood forSize versus JPEGBrowser supportWatch out for
JPEGPhotos, as the universal fallbackBaselineEverywhereNo transparency; visible blocks at low quality
PNGScreenshots, flat graphics, transparencyMuch larger for photosEverywhereNever use it for photos
WebPPhotos and graphics, lossy or lossless, with transparencyOften 25 to 35 percent smallerAll current browsersGains over a well-tuned JPEG can be small
AVIFPhotos, especially at low and medium qualityOften around halfAll current major browsersSlow to encode; no progressive display
SVGLogos, icons, simple illustrationsTiny for shapes, useless for photosEverywhereUser-uploaded SVG can carry scripts
GIFNothing newHuge for animationEverywhereReplace animated GIFs with video

Treat the size column with care. Savings depend on the photo, the encoder, and how you measure "the same quality". Before quoting a number, encode a sample of your own images and compare them at matched visual quality.

A matrix of five formats against five properties. JPEG: good for photos, no transparency, the baseline size, supported everywhere, fast to encode. PNG: huge for photos, has transparency, big files, supported everywhere, fast. WebP: good for photos, has transparency, smaller than JPEG, supported in all current browsers, fast. AVIF: best for photos, has transparency, the smallest files, supported in modern browsers, slow to encode. SVG: not for photos, has transparency, tiny for shapes only, supported everywhere, encoding does not apply.photosalphabytessupportencodeJPEGgoodnobaseallfastPNGhugeyesbigallfastWebPgoodyessmallerallfastAVIFbestyessmallestmodernslowSVGnoyestiny*alln/a
No row wins every column. AVIF trades encode time for bytes; SVG is tiny only for shapes, which is what the star means. That is why most sites serve AVIF with WebP or JPEG behind it.

Why not AVIF for everything?

AVIF usually wins on bytes, but encoding is often many times slower than JPEG or WebP, which matters for large upload volumes or first-request encoding at a CDN. AVIF files, as usually encoded, also do not display progressively: a progressive JPEG paints a blurry full image early and sharpens it, while an AVIF appears only once enough of it has arrived.

For most sites the answer is still AVIF first, with WebP and JPEG as fallbacks. For very large heroes on slow networks, test a progressive JPEG against AVIF and pick by measured LCP, not file size.

JPEG XL is a newer format with strong compression, lossless modes, and progressive display. Safari has supported it since version 17, and Chrome 155 and Firefox 158 turned it on by default in October 2026. Many users still run older versions, so for now add it as an extra picture source or let the CDN negotiate it, rather than making it the default. Chrome's team suggests testing both AVIF and JPEG XL, with JPEG XL most useful for high-fidelity and lossless images.

Quality settings and metadata

  • Quality numbers are not comparable across encoders. JPEG quality 80 and AVIF quality 80 mean different things. Tune each format separately, ideally against a perceptual metric such as SSIMULACRA 2 or Butteraugli, or let the CDN's automatic quality do it.
  • Strip metadata. Camera files carry EXIF data: camera model, date, and often GPS coordinates. Removing it saves bytes and protects the privacy of users who upload photos of their homes.
  • Apply orientation first. Phone photos are often stored sideways with an EXIF tag saying "rotate 90 degrees". If you strip the tag without rotating the pixels, the image shows sideways.

Decision How should the page get AVIF to browsers that support it and JPEG to the rest?

picture with type sources

Generate every format at build time or upload time.

List AVIF, then WebP, in source elements, with JPEG on the img.

Pros

  • Works with any static host or plain CDN.
  • Each URL is one fixed file, so caching is simple.

Cons

  • Long markup, repeated for every image.
  • Every format must be stored for every width.

CDN content negotiation

Use one URL per width, such as /img/shoe.jpg?w=800&format=auto.

The CDN reads the browser's Accept request header, which lists image/avif when supported, and returns the best format with Vary: Accept.

Pros

  • Plain img markup with one srcset.
  • New formats roll out without touching HTML.

Cons

  • One URL now has several cached copies; a shared cache that ignores Vary can serve AVIF to a browser that cannot show it.

One modern format only

Serve only WebP, which every current browser supports.

Pros

  • Simplest markup and storage.

Cons

  • Leaves AVIF's extra savings on the table.
  • Very old browsers get no image at all.

What we pick: With an image CDN, use content negotiation and keep the markup simple. Without one, use picture with AVIF and WebP sources and a JPEG img. Pick WebP-only for small sites where storage and build time matter more than the last 20 percent of bytes.

Check your understanding · Exercise 3

Which images are in the wrong format? Select all that apply.

Reserving space and showing something early

The third question is how much space the image takes. The browser lays out the page long before images arrive. If it does not know an image's height, it uses zero, then pushes the text below down when the file says how tall it is. That jump is a layout shift: the user aims for a link and taps an ad instead.

width and height give the aspect ratio

Put width and height attributes on every img, set to the intrinsic pixel size or any numbers with the same ratio. Browsers turn them into a default CSS aspect-ratio, so with a fluid width the height is known before a byte of the image arrives.

css
/* width and height attributes in the HTML: 1200 and 800 */
img {
  max-width: 100%;
  height: auto; /* keep the ratio when the width shrinks */
}

/* what the browser applies by default, in effect: */
/* aspect-ratio: auto 1200 / 800; */
The attributes do not fix the size. With height: auto, they only supply the ratio, so a 412 pixel wide box gets a height of about 275 pixels at once.

The auto in that default matters: once the real image loads, its own ratio wins. So if the attributes are slightly wrong the box still settles on the true shape, with a small shift, instead of stretching the photo.

Two phone screens while the hero image is still loading. Left, without width and height: the image box has zero height, so the headline and text sit right under the header, and when the image arrives they will jump down by about 275 pixels, a layout shift. Right, with width 1200 and height 800: the browser holds an empty 3 by 2 box at once, and the headline and text are already in their final place.no width or heightimage: 0 px tallheadline and textwill jumpwith width and height3:2 box heldlayout shift when it loadsnothing moves
Same HTML, one attribute pair apart. width and height with height: auto give the browser a ratio, so a 412 pixel wide box is about 275 pixels tall before any image byte arrives.

When CSS should own the box

In grids and cards, the design often fixes the shape: every product tile is square, whatever the seller uploaded. Set aspect-ratio: 1 / 1 in CSS and object-fit: cover so the photo fills the box without stretching. CSS reserves the space, and the image's own ratio no longer matters.

For user uploads of any shape, store width and height when the image is uploaded and send them with the feed data, so the client can render the attributes for images it has never seen. Reading dimensions after the image loads is too late: the shift has already happened.

Placeholders: something to look at while bytes arrive

Reserving space stops the jump, but the user still stares at an empty box. A placeholder fills that box with a cheap hint of the image, so the page looks finished sooner and the real image fades in rather than popping. There are four common levels, from cheapest to richest.

All four are computed once at upload or build time and stored next to the image's dimensions.
PlaceholderWhat it isCostNeeds JavaScript
Dominant colorOne background color picked from the photoA few bytes in CSSNo
LQIPLow Quality Image Placeholder: a tiny version, about 16 to 40 pixels wide, inlined as a data URL and blurred by CSSAbout 200 to 800 bytes per image in the HTMLNo
BlurHashA short text string, about 20 to 30 characters, that encodes a few color componentsTiny in the data, plus a small decoderYes, to draw it into a canvas or data URL
ThumbHashLike BlurHash, about 25 bytes, but also keeps the aspect ratio, transparency, and a bit more detailTiny in the data, plus a small decoderYes

The blur-up effect shows the LQIP scaled up with a CSS blur, then fades in the real image on top once it loads. BlurHash and ThumbHash do the same job with smaller payloads, which suits feeds and API responses where an inline image per item would bloat the JSON.

A state machine for one image slot. Reserved: an empty box with the right shape. On first paint it moves to Placeholder, showing a color, an LQIP, or a hash. When the image is eager or near the viewport it moves to Downloading. When all bytes are in, it moves to Decoded, a bitmap ready to draw. When decode resolves, it fades in and becomes Visible. If the download fails with a 404 or timeout, it moves to Error, which keeps the box and the placeholder and shows the alt text.Reservedempty box, right shapefirst paintPlaceholdercolor, LQIP, or hasheager, or near viewportDownloadingbytes on the wayall bytes inDecodedbitmap ready404 or timeoutErrorkeep box, show altdecode() doneVisiblefade in the photo
The box never changes size from the first state to the last, so none of these steps shifts the layout. Wait for img.decode() before the fade, and skip the fade under reduced motion.

Two traps. Inline LQIPs live in the HTML, so 40 of them add tens of kilobytes to the document; use them for the first screen and a color for the rest. And a hash placeholder needs its decoder JavaScript to run first, which can be later than a plain color would have shown.

Since Chrome 112, LCP ignores images with less than 0.05 bits of data per displayed pixel, such as a tiny blurred thumbnail stretched over a large box, so the real image still counts. A sharper or larger placeholder can cross that line and become the LCP element instead, which flatters your metric. Check the reported LCP element after adding one.

Decoding: decoding="async" and img.decode()

After download comes decoding: turning the file into a bitmap, which can take tens of milliseconds for a big photo on a slow phone. decoding="async" tells the browser it may paint the rest of the frame without waiting for this image; sync says wait, and auto, the default, lets the browser decide.

Browsers already decode most images off the main thread, so decoding="async" is a small, safe hint, not a big win. In JavaScript, await img.decode() resolves when an image is ready to paint. Use it before swapping a placeholder for the real image, so the swap does not flash.

Check your understanding · Exercise 4

A news feed shows user photos of any shape. The page jumps as photos load. Which fixes stop the jump? Select all that apply.

Image CDNs: variants on demand

Five widths times three formats is fifteen variants per image, plus crops. A build script can make them for a blog. A marketplace with forty million seller photos needs variants made on demand. That is what an image CDN does.

Transforms in the URL

An image CDN keeps the original once and treats the URL as a recipe. The path or query string says which original, what width, what format, what quality, and what crop. The first request for a recipe makes the variant; every later request for the same URL is a normal cache hit at the edge.

text
original:   s3://photos/listing/8812/front.jpg   (4032 x 3024, 3.9 MB)

recipe URL: https://img.example.com/listing/8812/front.jpg
              ?w=800        resize to 800 pixels wide
              &fit=cover    crop to fill the box
              &ar=1:1       square
              &fm=auto      best format the browser accepts
              &q=auto       quality chosen per image

result:     800 x 800 AVIF, about 60 KB, cached at the edge
A made-up but typical syntax. Cloudinary, imgix, Cloudflare Images, Akamai, and framework image services each have their own spelling of the same ideas.

Your markup then builds srcset by changing only the w parameter, usually in a small helper that takes an image id and a list of widths.

A sequence diagram with four actors: browser, CDN edge, transformer, and storage. The browser asks the edge for the shoe image at width 800 with format auto. The edge misses and asks the transformer to make the variant. The transformer gets the 3.9 MB original from storage, resizes and encodes it, and returns an 800 pixel AVIF of 60 KB to the edge, which caches it and returns it to the browser with Vary: Accept. Later, another browser asks for the same URL and the edge answers from its cache: a hit, 60 KB, with no transform.BrowserCDN edgeTransformerStorage?w=800&fm=automiss: make itget original3.9 MB JPEG800w AVIF, 60 KB60 KB, Vary: Acceptsame URL againhit: 60 KB
Only the first request for a recipe pays for the transform. That is why widths must be snapped to a fixed list: every new w value is a new first request.

Picking the format: Accept and Vary

With fm=auto, the CDN reads the Accept header the browser sends for images. Chrome, for example, has long sent image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8. If image/avif is listed, the CDN returns AVIF; if only WebP, it returns WebP; otherwise JPEG. The response carries Vary: Accept so any cache knows that the same URL has several versions.

Good image CDNs do not key their cache on the raw Accept string, which differs between browsers and would split the cache. They reduce it to a bucket (AVIF, WebP, or neither) and key on that. If you put your own CDN in front, check that it does the same, or it may serve a cached AVIF to an old browser.

Snap widths to a fixed list

If the frontend computes widths freely, from element.clientWidth for example, you get URLs for 411, 412, 413, and so on. Each is a separate cache entry that pays the transform cost, and the hit ratio collapses. Snap to a short fixed list and use it everywhere.

The same list is a security control. An open endpoint lets anyone request ?w=4999, ?w=4998, and so on, burning your transform budget. Reject widths not on the list, or sign URLs with an HMAC. And never let the endpoint fetch arbitrary remote URLs, or it becomes an open proxy; Next.js, for example, requires a list of allowed remote hosts.

Client hints, briefly

Chromium-based browsers can also send client hints, request headers such as Sec-CH-DPR and Sec-CH-Viewport-Width, after the server opts in with Accept-CH. Safari and Firefox do not send them, so treat them as an extra on top of srcset, never a replacement.

Decision When should the image variants be made?

At build time

A build step or bundler plugin resizes and encodes every image in the repository.

The HTML gets fixed URLs to static files.

Pros

  • No runtime service or per-request cost.
  • Works on any static host.

Cons

  • Build time grows with every image and format.
  • Useless for images uploaded after the build.

At upload time

When a user uploads, a background job makes a fixed set of variants and stores them.

The frontend picks from the known set.

Pros

  • Predictable storage and cost.
  • A good place to also compute dimensions and a placeholder hash.

Cons

  • Adding a new width or format means reprocessing every stored image.
  • You build and run the job yourself.

On demand at the edge

An image CDN makes each variant on its first request and caches it.

Pros

  • Any width, format, or crop is a URL change.
  • New formats roll out without reprocessing.

Cons

  • The first request for a variant is slower.
  • Ongoing vendor cost; needs width snapping and URL signing.

What we pick: Use build time for sites whose images live in the repo, and on-demand edge transforms for user uploads at scale. Upload-time variants fit teams that must keep data in their own infrastructure. Whichever you pick, compute dimensions and placeholders at upload.

Video, posters, and replacing GIFs

Video follows the same four questions with much bigger files. A 30 second product video is several megabytes. Downloaded before the user decides to watch, it competes with the hero and the scripts for bandwidth, and costs real money on a metered plan.

preload: how much to fetch before play

The preload attribute on video is a hint about how much to download before the user presses play. Like other hints, browsers may override it, for example on cellular data or in data-saver mode.

If you leave preload out, the default differs between browsers; the spec suggests metadata. Set it explicitly.
ValueWhat the browser fetchesUse it for
noneNothing until play, except the posterVideos below the fold, or many videos on one page
metadataThe first bytes: duration, dimensions, sometimes the first frame; usually tens to a few hundred KBA video the user may well play; shows the duration in the controls
autoAs much as the browser thinks is useful, possibly the whole fileRarely; a page whose main purpose is that one video
preload:
User:
A 30 second, 9.4 MB video on a 10 megabit connection with preload metadata. Before play the page downloads 230 KB: poster plus the file header. The user never plays, so 150 KB of video bytes are wasted.bytes before the user does anything230 KBposter plus the file headerscrolled past: 150 KB of video wastedvideo: 30 s at 2.5 Mbps = 9.4 MB, poster 80 KB
Illustrative numbers for one video. Browsers treat preload as a hint and may fetch less, for example on cellular data. The trade is bytes before play against wait after play.

preload metadata: 230 KB before play. 150 KB wasted if the video is never played.

The poster is an image: optimize it like one

The poster attribute names an image to show before playback. With preload="none", the poster is all the user sees, and when the video is the biggest thing on the first screen, the poster is often the LCP element. So everything earlier in this lesson applies: the right size, a modern format, and reserved space.

One gap: poster takes a single URL, with no srcset. Serve one sized for the common layout through an image CDN, or overlay a real img with srcset and remove it on play. Give the video width and height too, so its box is reserved.

html
<video
  controls
  preload="none"
  poster="https://img.example.com/demo-poster.jpg?w=1200&fm=auto"
  width="1280" height="720">
  <source src="/demo-720.webm" type="video/webm">
  <source src="/demo-720.mp4" type="video/mp4">
  <track kind="captions" src="/demo.en.vtt" srclang="en" label="English">
</video>
A video that costs one optimized poster image until the user presses play, with captions for people who cannot hear the audio.

Replace animated GIFs with video

GIF dates from 1987. It allows 256 colors per frame and has no real motion compression, so animations are huge. A short screen recording is often several megabytes as a GIF and a small fraction of that as MP4 or WebM, because video codecs store mostly what changes between frames.

To make a video behave like a GIF, use autoplay, loop, muted, and playsinline. Browsers reliably allow autoplay only when the video is muted, and playsinline stops iPhones from opening it full screen. Animated AVIF or WebP inside a plain img is another option when you want image semantics, such as in user-generated content that only accepts images.

Two rows of frames for the same 8-frame loop. The GIF row has 8 full frames of equal, large size, because GIF stores every frame whole with 256 colors: several megabytes in total. The video row has one key frame of full size followed by 7 small delta frames, because a video codec stores only what changed: a few hundred kilobytes in total.GIF: every frame stored whole, several MBfullfullfullfullfullfullfullfullVideo: one key frame, then only changes, a few hundred KBkeya key frame is a whole picture; a delta holds only what moved
Illustrative sizes. A video codec stores one whole picture now and then and only the changes in between, which is why the same clip is usually many times smaller as MP4 or WebM.

Check your understanding · Exercise 5

A recipe page has eight short how-to videos further down the page. Which setup keeps the first load light? Select all that apply.

Long videos: adaptive streaming

For long videos, streaming sites use adaptive bitrate streaming: the video is cut into segments of a few seconds, each encoded at several qualities, and the player picks the quality of each next segment from the measured connection. HLS and MPEG-DASH are the standard formats. Safari plays HLS natively, and recent Chrome versions have added native HLS too; elsewhere, and for DASH, a player such as hls.js or dash.js feeds segments through Media Source Extensions.

Failure modes, load order, and accessibility

Image bugs are quiet. Nothing throws an error when a phone downloads a 2400 pixel file for a 200 pixel slot; the page just gets slower. This chapter covers load order, what goes wrong in production, and accessibility.

Load order in one paragraph

The loading strategies lesson covers this in depth; the short version for images: the LCP image is a real img in the server-rendered HTML, never lazy, with fetchpriority="high". Images well below the fold get loading="lazy". Everything in between keeps the default.

Two network waterfalls for the same page on a 0 to 2 second axis. Top, with the hero image lazy-loaded at default priority: HTML from 0 to 300 ms, CSS from 300 to 600, JavaScript from 300 to 900, and the hero only starts at 800 ms after layout, finishing at 1500, so LCP is about 1.6 seconds. Bottom, with the hero eager and fetchpriority high: the preload scanner starts it at 250 ms, it finishes at 850, and LCP is about 0.9 seconds.hero lazy-loaded, default priorityHTMLCSSJSheroLCP about 1.6 shero eager, fetchpriority="high"HTMLCSSJSheroLCP about 0.9 s0 s0.5 s1 s1.5 s2 s
A lazy hero waits for layout before it even starts. Illustrative timings; the loading strategies lesson covers fetchpriority and loading in depth.
  • CSS background images start late. The preload scanner reads HTML, not CSS, so a background-image hero is found only after the stylesheet is parsed. CSS image-set() has no sizes either. Prefer an img with object-fit: cover.
  • Images rendered by JavaScript start later still. They wait for the bundle, the API call, and the render. Server-render the hero, or preload it.
  • Preloading a responsive image needs its own srcset. Use link rel="preload" as="image" with imagesrcset and imagesizes that match the img exactly. A plain href to one width makes some screens download two files: the preloaded one and the one srcset actually picks.

Symptoms, causes, fixes

SymptomLikely causeFix
Grid thumbnails are hundreds of KB eachsrcset with no sizes, so the browser assumes 100vwWrite an honest sizes, or sizes="auto" on lazy images
Images look soft on high density phonessizes claims a smaller slot than CSS draws, or the largest candidate is too smallFix sizes; check the rendered size against the file size in DevTools
The hero downloads twiceA preload href that does not match the candidate srcset picksPreload with imagesrcset and imagesizes
Content jumps as images arriveNo width and height, or art-directed sources with different ratios and no dimensionsAdd dimensions to the img and to each source
Uploaded photos appear sidewaysEXIF orientation stripped without rotating pixelsRotate first, then strip metadata
A broken image in an old browserA shared cache served a cached AVIF without respecting Vary: AcceptNormalize the format into the cache key
The image CDN bill jumpsUnbounded widths, or bots requesting random transformsSnap widths to a list; sign URLs
A long feed slows down and the tab crashesDecoded bitmaps of many large images in memoryRight-size images; virtualize the list

To find these, filter the DevTools Network panel to images and sort by size; hovering an image in the Elements panel shows its rendered size next to its intrinsic size. Lighthouse flags oversized images and older formats. In production, real-user monitoring with LCP attribution shows whether the LCP image's time goes to starting late, downloading, or rendering.

Check your understanding · Exercise 6

The team adds link rel="preload" as="image" href="/hero-1600.avif" for a hero whose img uses a srcset from 480w to 2400w. What happens on a phone that would pick 1200w? Select all that apply.

Accessibility: alt text and text in images

One image rule matters more than the rest: every img needs an alt attribute. Screen readers read it in place of the image, and browsers show it if the file fails to load.

  • Informative images get a short description of what matters in context: alt="Blue running shoe, side view", not alt="image" or the file name.
  • Decorative images get an empty alt="". Screen readers then skip them. Leaving alt out entirely is different and worse: many screen readers read the file name instead.
  • Images inside links or buttons describe the action: a magnifier icon in a search button gets alt="Search".
  • Text inside images cannot be resized, translated, or selected, and it is blurry when compressed. Put real text on top of the image with CSS instead. This also lets you use a more aggressive image quality, since there are no letter edges to protect.

Placeholders and fades need one more check: with prefers-reduced-motion, swap the image in without a long animation. And any lazy loading or placeholder logic must still leave a real img with alt in the DOM, not a div with a background, or assistive technology has nothing to read.

Check your understanding · Exercise 7

Which alt values are right? Select all that apply.

Using it in a system design interview

Images come up in almost every design question that shows content: a feed, a product listing, a gallery, chat attachments, a video platform. You rarely get a question about images alone; the interviewer watches whether you plan for them or forget them until asked.

When to bring it up

  • In the data model. Give each image an id, width, height, and a placeholder hash. That plans for layout stability before anyone asks.
  • In the API. Return image ids and dimensions, not fixed-size URLs; the client builds srcset from a shared width list.
  • In the performance section. Name the LCP image and how it gets priority; everything else is lazy with reserved space.
  • In the upload flow: originals to object storage, a job that reads dimensions, fixes orientation, strips metadata, and computes a placeholder, then an image CDN for delivery.

A full plan for a product grid

Here is what a strong answer sounds like for a marketplace search page with 24 product tiles in a 4-column grid on desktop and 2 columns on phones.

  1. Sellers upload originals to object storage; an upload job stores width, height, and a ThumbHash.
  2. An image CDN delivers them with automatic format and quality, widths snapped to 240, 360, 480, 720, and 960.
  3. Each tile is an img with that srcset, sizes="(min-width: 1024px) 25vw, 50vw", aspect-ratio: 1 / 1, and object-fit: cover.
  4. The first row loads eagerly, with fetchpriority="high" on the LCP tile; the rest use loading="lazy".
  5. Tiles show the ThumbHash as a placeholder, then fade in (no animation under reduced motion).
  6. Every tile has alt text from the product title.

Check the numbers out loud. A 412 pixel phone at DPR 2.625 has 206 pixel slots, so the target is about 540 pixels and each tile fetches 720w, maybe 40 to 70 KB as AVIF. Without sizes, each tile would fetch 960w.

Think first

In a social app, a user posts a photo and sees it in their feed immediately, before the image CDN has ever seen it. How do you avoid a blank or jumping tile?

How deep to go by level

LevelWhat the interviewer expects
JuniorKnows srcset exists, sets width and height, lazy-loads below the fold, writes good alt text.
Mid-levelExplains sizes and the DPR math, picks AVIF or WebP with a fallback, never lazy-loads the LCP image, uses an image CDN, plans placeholders.
SeniorDesigns the pipeline: upload processing, dimensions and hashes in the data model, width snapping, cache keys with Vary, URL signing, cost, memory limits on long feeds, video with streaming, and how to measure all of it.

Summary

  • Ask four questions of every image: how many pixels, which format, how much space, and when it loads.
  • Use srcset with w descriptors and an honest sizes; the browser picks the smallest candidate that reaches slot width times DPR. Missing sizes means 100vw.
  • Use picture for format fallback (type) and art direction (media), not for plain resolution switching.
  • AVIF is usually smallest, WebP is a safe middle, JPEG is the fallback; measure savings on your own images instead of quoting a number.
  • Put width and height on every img (and each art-directed source) so the browser reserves space; store dimensions at upload for user content.
  • Placeholders (color, LQIP, BlurHash, ThumbHash) make waiting feel shorter; keep inline ones to the first screen.
  • Image CDNs make variants from URL recipes; snap widths to a list, normalize the format in the cache key, and sign URLs.
  • Video: optimize the poster, use preload="none" or metadata, replace GIFs with muted looping video, and give motion a pause button.

In the interview

Questions interviewers ask about this. Answer each out loud first, then compare.

Question 1

Walk me through how the browser picks a file from srcset with width descriptors.

Question 2

When do you use picture instead of just srcset?

Question 3

Our product page has a CLS of 0.25 and most shifts come from images. What do you do?

Question 4

Design the image delivery for a marketplace where sellers upload millions of photos.

Question 5

A landing page has a 6 MB animated GIF in the hero and an autoplaying background video. How would you improve it?

Practice it

These design problems lean on images and media.

References

Read next