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 emit1x/2xdescriptors 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
-
coverfills the box and crops the overflow.containfits the whole image and letterboxes.insidefits 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, addwithoutEnlargement: true; withvips thumbnail, pass--size down. - Smart crop
-
Plain centre crop cuts faces off. Libraries offer attention-based crops (libvips
attentionandentropystrategies, exposed by sharp asposition: '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-ageplusstale-while-revalidate, and an ETag. -
If the server negotiates the format by
Accept, addVary: Acceptor 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.
References: MDN HTTP caching and MDN Cache-Control.
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.