2026-08-06 · 4 min read
Generating thumbnails at scale without wrecking your grid
How to pick thumbnail dimensions, why fill beats fit for grids, what srcset does for you, and how to keep a gallery of a thousand images fast.
A thumbnail grid is one of the few places where getting images slightly wrong is immediately visible. Ten photographs at inconsistent aspect ratios do not look like ten photographs; they look like a broken layout.
Consistency is the whole job
A grid is a repeated shape. When the images inside it are different shapes, the browser either lets them overflow their cells, letterboxes them with visible gaps, or crops them at whatever position the CSS happens to specify — and the result reads as untidy even to viewers who could not tell you why.
The fix is to decide the shape once and force every thumbnail into it, at processing time rather than in CSS. object-fit: cover will crop for you in the browser, but it does so after downloading the full image, and it gives you no control over what gets cropped.
Cropping during processing means you choose the focal point, and you ship smaller files.
Fill, not fit
For thumbnails specifically, fill is almost always correct: scale to cover the target box exactly and crop the overflow. Fit produces images that are smaller than the cell in one dimension, which reintroduces the inconsistency you were trying to remove.
Pad is the exception, and it is the right answer for one important case: product catalogues where cropping the product is unacceptable. Padding to a square on white gives you a consistent grid without cutting anything off, which is why ecommerce grids look the way they do.
Focal point matters more at small sizes
A centred crop discards the edges. On a full-size image that is often unimportant; on a thumbnail, where the subject occupies a smaller share of the visible area, it frequently removes the thing the image was of.
Two patterns cover most cases. People photography wants a top-weighted crop, because faces sit in the upper third and a centre crop decapitates full-length shots. Flat lays and low-sitting products want the opposite.
Setting this once for a batch and overriding the handful of frames where the subject sits somewhere unusual is far quicker than framing each one.
Choosing dimensions
Work from the largest size the thumbnail is ever displayed at, then double it for high-density screens.
| Grid | Typical display size | Export at |
|---|---|---|
| Dense contact sheet | 150 px | 300 px |
| Standard card grid | 300 px | 600 px |
| Two-column feature grid | 500 px | 1000 px |
| Product collection tile | 400 px | 800 px |
Beyond 2× there is no perceptible gain, and the file size cost is quadratic. A 3× thumbnail is 2.25 times the weight of a 2× one for a difference nobody can see.
Let srcset do the responsive work
If a thumbnail is 150 pixels wide on a phone and 400 on a desktop, shipping one 800-pixel file to both wastes most of a download on mobile — where the connection is worst.
<img
src="product-600.webp"
srcset="product-300.webp 300w, product-600.webp 600w, product-1200.webp 1200w"
sizes="(max-width: 640px) 45vw, (max-width: 1024px) 30vw, 300px"
width="600" height="600"
alt="Oak dining table"
loading="lazy">
The sizes attribute is the part people get wrong. It describes how wide the image will be in the layout, not how wide the file is, and the browser uses it to pick from srcset before layout is complete. Getting it wrong means the browser downloads the wrong candidate — usually a larger one than needed.
Generating three sizes per image is a batch operation: process the set at 300, again at 600, again at 1200, with a naming pattern that appends the width. {name}-{width} produces exactly the filenames the markup above expects.
Lazy loading, with one exception
loading="lazy" on grid images is close to free and can eliminate the majority of a gallery page's initial download.
The exception is anything above the fold. Lazy-loading your first row delays those images past the point where the browser would otherwise have started them, which directly harms Largest Contentful Paint. Load the first row eagerly and lazy-load the rest.
Reserve the space
Always set width and height on the element, even when CSS resizes it. Without them the browser cannot reserve space, so every image that loads shifts the content below it — a grid of forty images produces forty reflows and a Cumulative Layout Shift score that will show up in any audit.
Handling very large sets
For a gallery of a thousand images, the constraint stops being file size and becomes the number of DOM nodes and decoded images the browser is holding.
Two approaches work. Pagination is simple, robust and understood by everyone. Progressive rendering — mounting a page of items and adding more as a sentinel scrolls into view — feels better and costs an IntersectionObserver.
Full virtualisation, where only visible rows exist in the DOM, becomes worthwhile in the low thousands. Below that it is complexity you do not need, and it interacts badly with in-page search and anchor links.
A processing recipe
For a catalogue grid:
- Crop to square with a focal point suited to the subject.
- Resize to 300, 600 and 1200 in three passes.
- Convert to WebP at quality 80 — thumbnails tolerate more compression than full-size images because they are displayed small.
- Rename with
{name}-{width}so the markup can be generated mechanically.
That produces a complete responsive set from one source folder, with consistent framing, in four settings changes.