Image Layout Shift: Fixing It at the Pipeline Level
Adding width and height to an image tag by hand stops it moving the page, but only until the next upload, the next crop or the next theme change. On most sites the numbers should come from the image pipeline: the upload handler, the build step or the image service that already opens every file.
This guide is about making that pipeline measure each image once, correctly, store the result, and print it on every tag, so images reserve the right space on their own. It also covers the cases where recorded numbers go wrong: rotated phone photos, crops and art direction.
Why the pipeline should own image dimensions
A browser can only hold space for an image before it downloads if the HTML tells it the image's shape.
Current browsers turn the width and height attributes into a default aspect
ratio, so an image styled with width: 100%; height: auto gets a correctly shaped empty box
straight away. The mechanics of that, and the CSS that goes with it, are covered in the CLS Debugger
guide to images causing layout shift.
This page deals with a different question: where the numbers should come from.
On a small static site you can type them in. On anything with a CMS, a shop, user uploads or a content team, typed numbers fail in predictable ways:
- Nobody knows them. Editors upload a photo and pick it from a library; they never see its pixel size.
- They go stale. Someone replaces a 4:3 photo with a 16:9 one under the same file name, and the old numbers stay in the template.
- They are guessed. A developer writes
width="800" height="600"on a template that shows images of every shape. The box is right for some and wrong for the rest.
Every system that handles images already opens the file at some point: to make thumbnails, to convert formats, to upload it to storage. Reading the pixel dimensions at that moment costs almost nothing, and once they are stored alongside the image, every template can print them. That turns image layout shift from a recurring task into something that stays fixed.
Where to read true dimensions
Almost every image tool and content platform can tell you an image's width and height. The table shows where to get them in common setups.
| Setup | Where the dimensions come from |
|---|---|
| Node.js with sharp | await sharp(file).metadata() returns width,
height and orientation. |
| Python with Pillow | Image.open(path).size gives width and height; apply ImageOps.exif_transpose
first for phone photos. |
| Command line | magick identify -format "%w %h" photo.jpg with ImageMagick
7, or identify in older versions. |
| WordPress | Attachment metadata stores width and height for the original and each generated size, and core adds missing attributes to images in post content. |
| Headless CMS | Most return dimensions with the asset, for example Contentful under the file details and Sanity under the asset's metadata dimensions. |
| Hosted image services | Upload responses from services such as Cloudinary include width and height; store them rather than asking again on every page view. |
| Static site generators | Image processing in Hugo, the Eleventy Image plugin and the Astro image component can output width and height for local images at build time. |
The common thread is to take the numbers from the file itself, not from a form field or a naming convention.
A file called hero-1600x900.jpg is only a promise.
The rotated phone photo trap
Phones and many cameras do not physically rotate the pixels when you hold them upright. They save the image in the sensor's landscape orientation and add an EXIF orientation tag telling software to rotate it for display. Browsers honour that tag, so a portrait photo appears upright. But a tool that reads the raw pixel size reports it as landscape.
The result is a subtle shift. The pipeline records 4032 by 3024, the template prints those numbers,
the browser reserves a wide box, and when the image arrives the browser displays it upright, taller than
the box. With height: auto the browser then uses the real ratio, and everything below moves
down.
EXIF orientation values 5, 6, 7 and 8 mean the width and height are swapped for display. You have two safe options:
- Rotate the pixels at ingest and remove the orientation tag, so what is stored is
already upright. In sharp, calling
rotate()with no arguments does this using the EXIF value; in Pillow,ImageOps.exif_transpose; in ImageMagick,-auto-orient. - Swap the recorded numbers when the orientation is 5 to 8, and keep the original pixels. This works but every later step has to remember the rule, so option one is usually simpler.
// Node.js with sharp: normalise orientation, then measure
import sharp from 'sharp';
export async function ingest(inputPath, outputPath) {
const info = await sharp(inputPath)
.rotate() // apply EXIF orientation to the pixels
.toFile(outputPath); // strips the orientation tag by default
return { width: info.width, height: info.height };
}
When the image service changes the shape
Width and height only need to describe the ratio of the image that is actually delivered. If a 2400 by 1600 original is served at 800 by 533, the attributes can stay at 2400 and 1600, because the ratio is the same and CSS sets the display size. Problems start when something between storage and the browser changes the ratio.
Crops in image URLs
Image CDNs and platforms let you ask for a crop in the address, for example ?w=800&h=450&fit=crop
for a 16:9 card from a 3:2 original. If the template still prints the original's dimensions, the browser
reserves a 3:2 box and then receives a 16:9 image. Print the dimensions of the crop you asked for, which
you already know because you wrote them in the URL.
Responsive sets with mixed ratios
Every file in one srcset should share the same ratio. If the pipeline generates a 400
pixel version that is square and a 1200 pixel version that is wide, one set of attributes cannot be right
for both. Keep crops consistent within a set.
Art direction with the picture element
When a <picture> element deliberately shows a tall crop on phones and a wide one
on desktops, each <source> can carry its own width and height,
supported in current versions of the major browsers. Without them, every breakpoint uses the ratio from
the fallback <img>.
<picture>
<source media="(max-width: 600px)"
srcset="/img/workshop-tall-600.webp"
width="600" height="800">
<source srcset="/img/workshop-wide-1600.webp"
width="1600" height="900">
<img src="/img/workshop-wide-1600.jpg"
width="1600" height="900"
alt="Carpenter planing a table top in the workshop">
</picture>
Your pipeline should output the dimensions for each crop it generates, so the template can fill in each source without anyone measuring.
Checking whether recorded numbers match reality
The Image Optimisation audit flags images that contribute to layout shift. To see whether the cause is missing numbers or wrong numbers, paste this into the browser console on a loaded page. It lists images whose attribute ratio differs from the ratio of the file actually delivered.
[...document.images].forEach(img => {
const w = img.getAttribute('width'), h = img.getAttribute('height');
if (!w || !h) { console.log('NO SIZE', img.currentSrc); return; }
if (!img.naturalWidth) return; // not loaded yet
const declared = w / h, actual = img.naturalWidth / img.naturalHeight;
if (Math.abs(declared - actual) > 0.02) {
console.log('WRONG RATIO', declared.toFixed(3), actual.toFixed(3), img.currentSrc);
}
});
Scroll to the bottom first so lazy images have loaded. NO SIZE means the template never prints dimensions. WRONG RATIO means the numbers exist but describe a different image: a rotated original, an uncropped original, or a stale value from a replaced file. Group the results by template, because a wrong ratio on one product card usually means the same on all of them.
To confirm the effect, open the Performance panel in developer tools, record a page load with network throttling on, and look at the layout shift entries. Each one names the elements that moved, which lets you tie a shift back to a specific image.
The fix, step by step
Step 1
Find images whose attributes are missing or wrong
Run the Image Optimisation audit on each main template, then use the console check on a loaded page to list images with no width and height and images whose declared ratio differs from the delivered file. Note which template and which image field each one comes from. Missing numbers point to templates that never print them; wrong numbers point to the pipeline measuring the wrong file or not measuring at all.
Step 2
Measure each image once, when it enters the system
Add a measurement to the place every image already passes through: the upload handler, the build step or the image service integration. Read width and height from the file with the library you already use, such as sharp, Pillow or ImageMagick, or take them from the platform's asset response. Do it after any resize or crop the pipeline performs, so the numbers describe the file visitors receive.
Step 3
Correct for camera rotation before measuring
Phone photos often store landscape pixels with an EXIF tag saying display them upright.
Apply the orientation to the pixels at ingest, using rotate() with no arguments in sharp,
ImageOps.exif_transpose in Pillow or -auto-orient in ImageMagick, and then
measure. Otherwise portrait photos are recorded as landscape, the browser reserves a box that is too
short for them, and the content below jumps when the upright image finally arrives.
Step 4
Store the numbers with the image record
Save width and height next to the image wherever its address is stored: a database column, the asset's metadata, a JSON manifest from the build or the CMS field. Store the numbers for each derived crop as well as the original. Keeping them with the record means templates never need to open the file again, and replacing the file updates the numbers at the same moment.
Step 5
Print width and height from the stored values
Change every template that outputs an image to print width and height
from the stored record rather than typed constants. Include product cards, galleries, article bodies,
banners and images rendered from Markdown or rich text, which often need a render hook or filter. Keep
height: auto and a maximum width in the CSS so the image still scales to its container.
Step 6
Keep crops and art direction in step with the numbers
Where an image URL asks the CDN for a crop, print the dimensions of that crop, not the original. Make sure every file in one srcset shares a ratio. For art-directed picture elements, give each source its own width and height from the pipeline. Generate a placeholder colour or tiny preview at ingest too, so the reserved box is not blank while it waits.
Step 7
Backfill the existing library and re-measure
Run a one-off script over existing images to measure and store dimensions, correcting rotated photos as you go, and start with images used on the busiest templates. Clear page and CDN caches, run the console check again, and re-run the audit. Then watch CLS in field data from Search Console or your own real user monitoring over the next few weeks, since lab tests alone can miss slow connections.
Placeholders made at the same time
Reserving space stops the shift, but the reserved box is empty until the image arrives. The same pipeline step that measures an image can make something to show in the meantime, which improves how the page looks without affecting layout:
- A dominant colour. sharp's
stats()returns a dominant colour you can store and set as the image's background colour. - A tiny preview. A very small, blurred version of the image, a few hundred bytes, inlined as the background and replaced by the real image.
- A compact hash. Formats such as BlurHash and ThumbHash store a short string that a small script turns into a blurred preview.
None of these reserve space on their own. They sit inside a box whose size already comes from the width and height attributes. Keep them small: a placeholder that weighs as much as a thumbnail defeats the purpose.
Keeping it fixed
- Reject images without dimensions in the content model or build, rather than rendering them unsized. A build that fails loudly is better than a page that shifts quietly.
- Re-measure on replace. Make file replacement run the same ingest step as a new upload, so a new shape brings new numbers.
- Test with a portrait phone photo whenever the upload code changes, because it is the case most likely to break.
- Add the console check to a test in your end-to-end suite for key templates.
- Watch field CLS after theme and plugin updates, which sometimes replace image markup.
For the responsive sizes themselves, see fixing wrong image dimensions, and for lazy-loaded images, fixing lazy-loading.
1. Why do my images still shift after adding width and height?
Most often because the numbers describe a different shape from the file the browser receives. Common reasons are a portrait phone photo recorded as landscape because of EXIF rotation, a CDN crop that changes the ratio while the tag keeps the original numbers, a stale value left after the file was replaced, or a srcset whose files have different ratios. Compare the declared ratio with the loaded image's natural size in the console to find which one applies.
2. Do width and height need to match the file served to each device?
No, only the ratio needs to match. If the original is 2400 by 1600 and a phone receives an 800 by 533 version from the srcset, the attributes can stay at 2400 and 1600, because both describe the same 3:2 shape and CSS controls the displayed size. What must not happen is a change of shape between the declared numbers and the delivered file, such as a crop to a different ratio.
3. What is EXIF orientation and why does it affect layout shift?
Many phones save photos in the sensor's landscape orientation and add an EXIF tag telling software to rotate them for display. Browsers follow the tag, so the photo looks upright, but a tool that reads the raw pixels reports landscape width and height. If those numbers are printed on the tag, the browser reserves a wide box and then shows a tall image. Rotating the pixels at upload and then measuring prevents it.
4. Can a CMS add image dimensions automatically?
Many can, as long as they store the dimensions for each asset. WordPress records them in attachment metadata and adds missing attributes to images in post content, headless systems such as Contentful and Sanity return them with the asset, and static site tools such as Hugo, Eleventy and Astro can output them at build time. The gaps are usually custom templates, page-builder widgets and images inserted from external URLs, which need checking one by one.
5. How do I set dimensions for images inside a picture element?
Put width and height on the img element, which is used for the fallback and by default for every source. If your sources show different crops at different screen sizes, add width and height to each source element as well, describing that source's ratio. Current versions of the major browsers use those values to reserve the right space at each breakpoint. Your image pipeline should output the dimensions for every crop so the template can fill them in.
6. Do image placeholders like BlurHash stop layout shift?
Not on their own. A placeholder is something to show inside the space while the real image downloads; it does not decide how big that space is. The size still has to come from width and height attributes or a CSS aspect ratio. Used together, the attributes reserve the correct box and the placeholder fills it with a colour or blurred preview, which looks better than an empty rectangle. Keep placeholders tiny so they do not slow the page.
7. Is it worth backfilling dimensions for thousands of old images?
Usually yes, and it is less work than it sounds because a script can do it. Read each stored image, apply its orientation, record the width and height, and write them back to the database or asset metadata. Start with images used on the busiest templates such as product listings and the home page, where shifts affect the most visits. Once the backfill is done, the upload pipeline keeps new images covered automatically.
The Image Optimisation audit checks every image on your page for its contribution to layout shift, along with file size, format, dimensions, lazy-loading, alt text and CDN delivery, and ranks its recommendations by their effect on loading and bandwidth.
Open Image Optimisation Fixes โ