Responsive Images and Thumbnails

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

See the section bibliography for the reference material consulted while preparing these pages.

Baseline date: 2026-10-07. A backend that stores one big image per upload makes every client download too much. The fix is a small set of precomputed or on-demand variants, described here in terms of what the server produces. The HTML that consumes them is in native HTML images.

Variant strategy

Width breakpoints

Choose a short list of widths that cover the slots in your layout, for example 320, 640, 960, 1280 and 1920. Each extra width multiplies storage and encode work. Pick widths from real slot sizes, not from device widths, and prefer roughly 1.5x steps over very fine ones, because the saving between two close widths is small.

Density variants

A 2x display needs twice the pixels in the same CSS slot. Either list a larger width in srcset (width descriptors, recommended) or emit 1x/2x descriptors for fixed-size images such as avatars.

Aspect-ratio crops

Thumbnails in a grid need identical aspect ratios, or the layout jumps. Decide the ratio in the product (for Bookshelf, covers are 2:3) and crop on the server, not with CSS only, so the bytes sent match the pixels shown.

Fit modes

cover fills the box and crops the overflow. contain fits the whole image and letterboxes. inside fits the image within the box, keeping its aspect ratio, and is the usual choice for article images. None of these modes prevents upscaling on its own. Never upscale: a small original gets a variant at its own size, not a blurry big one. In sharp, add withoutEnlargement: true; with vips thumbnail, pass --size down.

Smart crop

Plain centre crop cuts faces off. Libraries offer attention-based crops (libvips attention and entropy strategies, exposed by sharp as position: 'attention'), or you can send a focal point from the client, or call an image-analysis service. Store the focal point with the image so later variants stay consistent.

Naming and storage keys

Use deterministic keys that encode what the variant is, so no database lookup is needed to compute a URL:

covers/<id>/original.jpg        private bucket, with EXIF, never public
covers/<id>/w320.avif
covers/<id>/w320.webp
covers/<id>/w320.jpg
covers/<id>/w640.avif
...

Include a content hash or version in the key when the image can change (w640-3f9a1c.avif), so each URL is immutable and can be cached forever. Never overwrite a key that a CDN may have cached: change the key instead.

Eager versus on-the-fly generation

Eager (at upload) On-the-fly (at first request)

How

A worker produces all variants after upload, see upload pipelines.

An image service or edge function produces a variant when it is first requested and a CDN caches it.

Pros

Predictable latency, no compute on the read path, easy to audit.

No wasted variants, new sizes need no backfill, one URL scheme for every size.

Cons

Storage for variants nobody requests, backfill job for new breakpoints.

First request is slow, and an open resizing endpoint is an abuse and cost risk. Sign the URLs or whitelist sizes.

Fits

A fixed set of sizes, user-generated content.

Large catalogues, editorial sites, changing layouts.

Mixed strategies are common: eagerly produce a small and a large variant, and resize everything else on demand. Managed on-demand options are covered in the cloud pages: AWS, Google Cloud and Azure. Image CDNs such as Cloudinary and imgix accept transformations in the URL.

Cache headers and CDN invalidation

  • Immutable, content-addressed variants: Cache-Control: public, max-age=31536000, immutable.

  • Variants at a stable URL that can change: a short max-age plus stale-while-revalidate, and an ETag.

  • If the server negotiates the format by Accept, add Vary: Accept or the CDN may serve the wrong format.

  • Prefer new keys to invalidation. Invalidation by path is slow, billed by some CDNs and still leaves browser caches untouched.

What the backend must emit for srcset, sizes and picture

The browser chooses the file, but it can only choose among what the API describes. Return the list of variants with the image metadata, so the frontend can build the markup without guessing URLs:

{
  "id": "b81f",
  "width": 2400,
  "height": 3600,
  "blurhash": "LKO2?U%2Tw=w]~RBVZRi};RPxuwH",
  "variants": [
    { "format": "avif", "width": 320, "url": "https://cdn.example.com/covers/b81f/w320.avif" },
    { "format": "avif", "width": 640, "url": "https://cdn.example.com/covers/b81f/w640.avif" },
    { "format": "webp", "width": 640, "url": "https://cdn.example.com/covers/b81f/w640.webp" },
    { "format": "jpeg", "width": 640, "url": "https://cdn.example.com/covers/b81f/w640.jpg" }
  ]
}

Always include the intrinsic width and height of the original (or of the aspect ratio) so the page can reserve space and avoid layout shift, and the BlurHash for the placeholder, see BlurHash. The client then builds:

<picture>
  <source type="image/avif" srcset="…/w320.avif 320w, …/w640.avif 640w" sizes="(min-width: 60em) 20vw, 50vw">
  <source type="image/webp" srcset="…/w320.webp 320w, …/w640.webp 640w" sizes="(min-width: 60em) 20vw, 50vw">
  <img src="…/w640.jpg" width="2400" height="3600" alt="Cover of Moby-Dick" decoding="async">
</picture>

sizes describes the slot width, not the image width, and only the page author knows it. References: MDN responsive images and MDN picture element.