A layout shift from an unsized image is one of the most common Core Web Vitals failures, and it's entirely preventable. The browser doesn't know how tall an <img> is until the file downloads and decodes. Without a reserved box, everything below the image collapses upward, then jumps back down the moment the image paints. That jump is measured directly by Cumulative Layout Shift, and it's one of the easier Core Web Vitals problems to fix permanently.
Why Unsized Images Cause Shift
When a browser parses HTML, it builds the layout tree incrementally, before every resource has loaded. An <img> with no size information gets a zero-height box by default. As soon as the image response arrives and the browser learns the real dimensions, it re-flows the page: paragraphs shift down, buttons move, sometimes a shift happens right as a user is about to tap something else.
This is worse on slow connections and mid-range mobile devices, exactly where perceived instability matters most. It's also compounding — a page with five unsized images can rack up five separate shift events, each one counted against your CLS score.
The Core Fix: Always Declare Intrinsic Dimensions
The single most reliable fix is to always include width and height attributes on every <img> tag, sourced from the actual image file's intrinsic dimensions:
<img
src="/images/hero.jpg"
width="1600"
height="900"
alt="Product dashboard overview"
loading="lazy"
/>
Modern browsers use these attributes to calculate the aspect ratio and apply it via the UA stylesheet, so the space is reserved even before CSS loads. This behavior is documented in the HTML spec's rendering rules for replaced elements, and it's why the width/height attributes still matter in a CSS-heavy world.
Combining width/height With Responsive CSS
Most sites size images fluidly with CSS rather than letting them render at fixed pixel dimensions. That's fine — pair the intrinsic attributes with aspect-ratio so the box is reserved regardless of the rendered width:
img {
width: 100%;
height: auto;
aspect-ratio: attr(width) / attr(height);
}
Support for attr() in non-content CSS properties is inconsistent across browsers, so the safer, currently-supported pattern is to let the browser derive the ratio automatically from the HTML attributes — which it already does once height: auto is set alongside explicit width/height attributes. You generally don't need to write aspect-ratio manually if width/height attributes are present; the browser computes it for you per the CSS Sizing spec's default sizing algorithm.
Where aspect-ratio earns its keep is background-driven layouts, <div>-based image placeholders, or elements with no natural intrinsic size, like a container that will receive a lazy-loaded background image:
.thumbnail {
aspect-ratio: 16 / 9;
background-color: #e5e7eb; /* placeholder tone while loading */
overflow: hidden;
}
Handling Images With Unknown Dimensions at Build Time
User-uploaded images or CMS content often arrive without known dimensions until runtime. Three practical approaches, in order of preference:
- Resize and normalize on upload. Run every uploaded image through a fixed pipeline — resize to a canonical width, and store the resulting width/height alongside the asset record so the dimensions are always known before render. The Image Resizer is useful for testing this normalization step manually before wiring it into a pipeline.
- Probe dimensions server-side and cache them. If you can't control upload-time processing, read image headers once (most formats expose dimensions in the first few bytes) and cache the result so every subsequent render has the values ready.
- Reserve a fixed aspect-ratio container as a last resort. If dimensions genuinely can't be known ahead of render, pick a sensible default ratio for the content type (e.g., 4:3 for product photos) so the shift, if any, is contained to a size correction rather than a collapse-from-zero.
Framework-Specific Notes
Next.js's next/image component requires either width/height or fill with a sized parent specifically to prevent this class of bug — it will throw a build warning if dimensions are missing, per the Next.js Image component docs. If you're on plain HTML or a framework without that guardrail, you own this responsibility manually, and it's worth adding a lint rule or PR checklist item enforcing it.
Decision Point: When Not to Bother With Precise Dimensions
Not every image needs pixel-perfect intrinsic sizing. If an image sits inside a container that's already sized by other content — a fixed-height card grid, a carousel with a hard-coded viewport height — the container itself prevents shift regardless of what the image does. In that case, spending time hunting down exact source dimensions is wasted effort; just make sure the image has object-fit: cover so it fills the pre-sized box cleanly without stretching:
.card img {
width: 100%;
height: 100%;
object-fit: cover;
}
Reserve the width/height + aspect-ratio discipline for images that sit in normal document flow, where nothing else constrains their box — that's where unsized images actually cause visible jumps.
Verifying the Fix
After applying attributes, confirm the fix rather than assuming it worked. Open Chrome DevTools' Performance panel, record a page load, and look for "Layout Shift" entries in the Experience track — each one lists the exact element responsible. A clean fix shows zero shift events attributable to images. Lighthouse's CLS metric in the same panel gives you a single number to track over time; run it before and after your change on the same throttled network profile so the comparison is meaningful, not just theoretical.
If you're preparing images for the web and want a quick pre-upload pass — resizing to your target dimensions and converting to a more efficient format in one step — the PNG to WebP converter handles both concerns together, so the dimensions you record for your HTML attributes match exactly what ships to the browser.