Images without explicit width and height cause 66% of CLS issues on mobile. Every image missing dimensions is a layout shift waiting to happen.
When you load an image without telling the browser its size, the browser reserves zero space for it. The image downloads, the browser discovers its actual dimensions, and everything below it gets shoved down. That's a layout shift.
This happens because browsers render content progressively. They don't wait for images to download before showing text. So they start laying out the page with a 0x0 placeholder for your image. When the image finally loads, the placeholder expands to fit it, and content reflows.
The technical flow:
<img> with no dimensionsThe larger the image and the higher it appears on the page, the worse the shift. A hero image without dimensions can cause CLS of 0.3+ on its own - three times over the "poor" threshold.
Open Elements panel and inspect your images. Look for:
width attributeheight attributewidth: auto or height: auto without an aspect-ratioQuick console check:
document.querySelectorAll('img').forEach((img) => {
const hasWidth = img.hasAttribute('width') || img.style.width
const hasHeight = img.hasAttribute('height') || img.style.height
const hasAspectRatio = getComputedStyle(img).aspectRatio !== 'auto'
if (!(hasWidth && hasHeight) && !hasAspectRatio) {
console.warn('Unsized image:', img.src, img)
}
})
The audit "Image elements do not have explicit width and height" lists every offending image with its URL and DOM location. Each image in that list is causing or could cause a layout shift.
Lighthouse checks that images have either:
width and height HTML attributes with valid non-negative integerswidth and height that aren't auto/initial/unset/inheritaspect-ratioImages with position: fixed or position: absolute are excluded since they don't affect document flow.
The simplest fix. Add width and height to every <img> element:
<!-- Before: CLS disaster -->
<img src="/hero.jpg" alt="Hero image">
<!-- After: No CLS -->
<img src="/hero.jpg" width="1200" height="630" alt="Hero image">
These attributes tell the browser the image's intrinsic dimensions. Modern browsers use them to calculate the aspect ratio and reserve the correct space before the image loads.
For responsive images, combine attributes with CSS:
<img
src="/hero.jpg"
width="1200"
height="630"
class="w-full h-auto"
alt="Hero image"
>
.w-full { width: 100%; }
.h-auto { height: auto; }
The browser calculates: "This image has a 1200:630 (1.9:1) aspect ratio. At 100% width, I'll reserve height accordingly." No shift.
When you can't add HTML attributes (CMS limitations, dynamic images), use CSS:
.hero-image {
aspect-ratio: 16 / 9;
width: 100%;
}
.avatar {
aspect-ratio: 1;
width: 48px;
}
.product-image {
aspect-ratio: 4 / 3;
width: 100%;
object-fit: cover;
}
aspect-ratio tells the browser the proportions directly. Combined with one dimension (width or height), it calculates the other.
Fallback for older browsers:
.hero-image {
aspect-ratio: 16 / 9;
width: 100%;
}
/* Fallback using padding trick */
@supports not (aspect-ratio: 16 / 9) {
.hero-image-wrapper {
position: relative;
padding-bottom: 56.25%; /* 9/16 = 0.5625 */
}
.hero-image-wrapper img {
position: absolute;
width: 100%;
height: 100%;
object-fit: cover;
}
}
When dimensions are truly unknown (user uploads, external URLs), use container constraints:
<div class="image-container">
<img src="/user-upload.jpg" alt="User content">
</div>
.image-container {
aspect-ratio: 1; /* Force square */
width: 100%;
overflow: hidden;
background: var(--ui-bg-muted);
}
.image-container img {
width: 100%;
height: 100%;
object-fit: cover;
}
The container reserves space. The image fills it. Content around it never shifts.
For galleries with mixed aspect ratios, pick a dominant ratio:
.gallery-item {
aspect-ratio: 4 / 3; /* Most photos are roughly this */
background: var(--ui-bg-muted);
}
.gallery-item img {
width: 100%;
height: 100%;
object-fit: contain; /* or cover, depending on preference */
}
Browser aspect ratio calculation happens during the layout phase, before paint. When you provide dimensions (via attributes or CSS), the browser knows exactly how much space to reserve during initial layout.
The key insight: you're not preventing the image from loading - you're preventing the layout from changing when it does.
next/image requires dimensions and prevents CLS by design:import Image from 'next/image'
// Static import - dimensions inferred
import heroImage from './hero.jpg'
<Image src={heroImage} alt="Hero" />
// External URL - dimensions required
<Image
src="https://example.com/photo.jpg"
width={800}
height={600}
alt="Photo"
/>
// Fill mode - container controls size
<div className="relative w-full aspect-video">
<Image src="/hero.jpg" fill alt="Hero" />
</div>
srcset and handles responsive sizing. Using raw <img> in Next.js usually means missing out on these protections.NuxtImg from @nuxt/image handles dimensions:<template>
<NuxtImg
src="/photo.jpg"
width="800"
height="600"
alt="Photo"
/>
</template>
<NuxtImg
src="/hero.jpg"
width="1200"
height="630"
sizes="sm:100vw md:50vw lg:800px"
alt="Hero"
/>
<div class="aspect-video w-full">
<NuxtImg
:src="dynamicUrl"
class="w-full h-full object-cover"
alt="Dynamic content"
/>
</div>
After adding dimensions:
1. Re-run Lighthouse
The "Image elements do not have explicit width and height" audit should pass. If images still appear, they're missing proper sizing.
2. Check with DevTools
// Should return empty array
document.querySelectorAll('img').filter((img) => {
const style = getComputedStyle(img)
const hasExplicitSize
= (img.width && img.height)
|| (style.width !== 'auto' && style.height !== 'auto')
|| style.aspectRatio !== 'auto'
return !hasExplicitSize
})
3. Visual test
Throttle network to Slow 3G, reload the page. Watch the image areas. They should have placeholder space that fills in when images load - no jumping.
Expected improvement: Fixing a single unsized hero image can reduce CLS from 0.25+ to under 0.1. Multiple unsized images compound, so fixing all of them often results in CLS dropping to near zero from this source.
Only adding width
<!-- Wrong - height is still auto -->
<img src="/photo.jpg" width="800">
<!-- Right -->
<img src="/photo.jpg" width="800" height="600">
Both dimensions are required for aspect ratio calculation.
Wrong aspect ratio
<!-- Image is actually 1200x800, not 1200x600 -->
<img src="/photo.jpg" width="1200" height="600">
This causes the image to stretch or compress. Use actual image dimensions or matching aspect ratio.
Forgetting lazy-loaded images
<!-- Still needs dimensions even with lazy loading -->
<img src="/photo.jpg" loading="lazy" width="800" height="600">
Lazy loading defers download, not space reservation. Dimensions are still critical.
Using percentage for both dimensions
/* Doesn't help - browser still doesn't know absolute size */
img {
width: 100%;
height: 100%;
}
The parent needs a defined size, or use aspect-ratio with one dimension.
Unsized images often appear alongside:
One page might pass. Another might have a template with unsized images that repeats across hundreds of URLs. Unlighthouse scans your entire site and surfaces every page with unsized image issues, sorted by CLS impact.