Preventing Image Layout Shift with aspect-ratio
An <img> with no reserved height paints at zero height, then jumps to its full size the instant the bitmap decodes β forcing a second layout pass that shoves every following element down the page and registers as a Cumulative Layout Shift.
This guide is part of Intrinsic Sizing and Aspect Ratio, a topic within the Layout and Paint Optimization pillar. If you have watched a hero image snap into place a beat after the text renders and dragged the readerβs paragraph out from under their cursor, you have seen the exact symptom this page fixes: an unsized replaced element that resolves its geometry late, in the layout phase, after the surrounding box tree has already been positioned.
The Minimal Reproduction
The smallest reliable trigger is an image with a source but no dimensional hints. The browser has nothing to reserve space with until the response body arrives and the intrinsic size is decoded.
<!doctype html>
<article>
<h1>Trip report</h1>
<img src="/photo-1600x900.jpg" alt="Ridge at dawn"> <!-- BAD: no width/height, no aspect-ratio -->
<p>The climb took four hours and the view was worth every stepβ¦</p>
</article>
img { max-width: 100%; } /* height stays 'auto' β box collapses to 0 until decode */
The paragraph renders immediately, flush under the heading, because the image box is zero pixels tall. Milliseconds later the JPEG decodes, the browser learns the image is 1600Γ900, reserves 900 CSS pixels (or the scaled equivalent), and re-runs layout. Everything below the image moves down by that amount. That downward movement of already-painted content is what the Layout Instability API scores as a shift.
Why the Box Collapses Then Jumps
A replaced element like <img> carries an intrinsic size β the natural pixel dimensions baked into the resource. On the first layout pass, before the image resource has been fetched and its header decoded, the layout engine (Blinkβs LayoutNG, WebKitβs layout tree, Geckoβs reflow) has no intrinsic dimensions to consult. With height: auto and no width/height attributes, the used height resolves to zero, so the box contributes nothing to the block flow. Layout completes, paint runs, and the frame ships with the image occupying no vertical space.
The image bytes arrive on a network thread, get handed to the image decoder, and the decoded dimensions are posted back to the main thread as a resource-load event. That event dirties the layout tree: the imageβs box is marked for re-layout, which cascades to every sibling and ancestor whose position depends on it. The main thread runs a second layout, the block flow expands, and the compositor receives a new set of positions. Because the first frame was already visible to the user, the delta between the two layouts is recorded by the browserβs layout-shift bookkeeping as an unstable region, weighted by the fraction of the viewport that moved.
The cost is not the second layout itself β layout is fast for a handful of nodes β it is that the second layout happens after a paint the user has already seen. Reserve the space up front and the second layout still runs when the image decodes, but it changes nothing geometrically, so no shift is recorded.
It helps to see where each piece of this lands in the pipeline and what it costs. The shift is not a single expensive operation; it is a cheap layout whose timing relative to the first paint is what turns it into a visible defect and a scored instability.
| Pipeline phase | Constraint | Cost |
|---|---|---|
| Parse / box construction | No intrinsic size until the resource header decodes | <img> box built with used height 0 |
| Layout pass 1 | height: auto resolves to 0 with nothing to size against |
Block flow packs siblings flush against the heading |
| Image decode | Runs off-thread, posts dimensions back to the main thread | Marks the image box and its ancestors dirty |
| Layout pass 2 | Re-run after a visible paint | Expands block flow, moves every following element |
| Layout shift scoring | Impact fraction Γ distance fraction of moved region | Non-zero CLS attributed to content below the image |
Reading the Instability in a Trace
In a Performance recording, the tell is a Layout Shift event landing shortly after an image resource finishes, with a non-zero score attributed to a region below the image. The paired signature is two Layout nodes for the same subtree separated by an Image Decode, where the second layout produces a different block height. Here is the annotated shape as it appears when you expand the main-thread track.
Main thread β recording (unsized image)
β
ββ Parse HTML ................................ 1.2 ms
β ββ create <img> box (intrinsic size unknown)
ββ Layout ................................... 0.6 ms β pass 1: img height 0
ββ Paint + Composite ........................ 2.1 ms β user sees text at top
β
β (network) GET /photo-1600x900.jpg ........ 180 ms
β
ββ Image Decode ............................. 3.4 ms β intrinsic 1600Γ900 resolved
ββ Layout ................................... 0.7 ms β pass 2: reserve 900px β
β ββ dirtied: <img>, <p>, <article> β¦
ββ Layout Shift ............................. score 0.19 β CLS
region: everything below the image, moved +506px
The β
layout is the reflow you want to make geometrically inert. The decision about how to reserve the space depends on what you know at author time β a fixed intrinsic size, a responsive fluid width, or an unknown ratio you must impose. The tree below maps those cases to the right declaration; each one gives the layout engine a height on pass one so pass two moves nothing.
The Fix
There are two correct ways to pre-reserve the box, and they compose. The oldest and most robust is to put width and height attributes on the element. Modern engines read those attributes and synthesize a default aspect-ratio for the box, so even when your CSS sets width: 100%; height: auto, the browser scales a correctly proportioned placeholder before the bytes land. The second way is to declare aspect-ratio directly in CSS, which is what you reach for when the markup cannot carry dimension attributes (background-driven art, a <picture> with art direction, or a CMS that strips them).
/* BEFORE β box collapses to 0 until decode β CLS on image load */
img {
max-width: 100%;
height: auto; /* auto with no intrinsic size yet = 0px reserved */
}
<!-- AFTER β attributes give the UA a ratio to reserve space immediately -->
<img
src="/photo-1600x900.jpg"
width="1600"
height="900"
alt="Ridge at dawn"
> <!-- UA computes aspect-ratio: 1600 / 900 from these attributes -->
/* AFTER β CSS keeps it fluid without discarding the reserved ratio */
img {
max-width: 100%;
height: auto; /* now scales the ratio, not to 0 */
aspect-ratio: 16 / 9; /* explicit fallback when attrs are absent */
/* holds a 16:9 box before the first byte decodes β no second-pass shift */
}
The attributes and the CSS aspect-ratio agree here, so the layout engine reserves a 16:9 box on the very first layout pass. When the decode event fires, the box already has the correct proportions; the second layout confirms the geometry instead of changing it, and no shift region is emitted. Note the ordering trap: if you set aspect-ratio but override height to a fixed non-auto value, the aspect ratio is ignored for the used height, so keep height: auto whenever you rely on the ratio.
For genuinely unknown ratios β user-uploaded images where the server does not measure dimensions β reserve a conservative placeholder ratio and pair it with object-fit: cover so the decoded image fills the reserved box without distortion. That trades a small crop for a stable layout, which is almost always the right bargain because the reserved box means the second layout never reaches the reflow triggers that move sibling content.
Because the reserved box removes the geometry change, this fix also keeps the second layout off the critical path for interaction: no sibling repositioning means the forced synchronous layout risk from any script measuring offsetTop right after the image loads disappears too. If your images are inside a list or feed rendered by JavaScript, combine this with disciplined read/write batching so the reserved boxes are not immediately invalidated by measurement code.
Verification Checklist
Frequently Asked Questions
Do width and height attributes still work if my CSS sets width to 100%?
Yes. Modern engines translate width and height attributes into a default aspect-ratio for the box. When your CSS sets width: 100%; height: auto, the browser scales that ratio to the available width and reserves the correct height before the image decodes, so the attributes and fluid CSS cooperate rather than conflict.
Why does aspect-ratio get ignored on some of my images?
aspect-ratio only controls the missing dimension. If you set both width and a fixed non-auto height in CSS, the height wins and the ratio is discarded. Keep height: auto (or leave one axis unconstrained) whenever you rely on aspect-ratio to reserve the box.
What ratio should I use for user-uploaded images with unknown dimensions?
Measure them server-side if you can and emit real width/height attributes. If you cannot, pick a conservative placeholder ratio such as 4 / 3, apply it with aspect-ratio, and add object-fit: cover so the decoded image fills the reserved box. A small crop is almost always cheaper than a layout shift.
Does aspect-ratio help with lazy-loaded images below the fold?
Yes, and it matters more there. A lazy image that decodes as it scrolls into view will shift content unless its box was reserved up front. Setting aspect-ratio (or dimension attributes) means the reserved height exists from first layout, so scrolling into the image never repositions the surrounding content.
Related Guides
- Contain-Intrinsic-Size and Scroll Anchoring β reserve space for content-visibility subtrees the same way aspect-ratio reserves it for images.
- Reducing Layout Shift from Web Fonts β the text-side counterpart to unsized images causing CLS.
- Debugging CLS with the Layout Instability API β attribute each shift score to the element that moved.
- Which CSS Properties Trigger Reflow vs Repaint β understand which second-pass changes force the layout that moves content.