Fixing image dimension errors and CLS: a developer's guide

Fixing image dimension errors and CLS: a developer's guide

Last verified: October 13, 2026
19 min read
Guide
Technical SEO
Full-stack developer

Image dimension warnings and Cumulative Layout Shift (CLS) are among the most frequent findings in Google Search Console and PageSpeed Insights. CLS is the Core Web Vitals metric for visual stability, and missing or incorrect image dimensions are one of its most common causes. This guide shows how to find the causes in WordPress, fix templates and the media pipeline, satisfy Google Discover image requirements, protect Largest Contentful Paint (LCP), and measure CLS repeatably instead of turning one URL green.

A layout shift happens whenever a visible element changes its start position from one rendered frame to the next. Missing width/height on <img>, late hero images, cookie banners and ad slots without reserved height are the usual suspects. Official references: web.dev on CLS, web.dev on optimizing CLS, MDN aspect-ratio and wp_get_attachment_image().

The same pattern shows up in shops and magazine themes again and again: the custom theme prints images with hard-coded markup, editors upload 5000-pixel originals, and an optimization plugin compresses files without ever writing dimensions into the HTML. PageSpeed then turns green on a demo URL while CrUX stays red, because category and product templates use different image paths.

Learn more about our WordPress speed optimization services.

#What image dimension errors mean in PageSpeed Insights

For years Lighthouse reported “Image elements do not have explicit width and height” when the HTML carried no dimensions. The browser then learns the aspect ratio only after the download and pushes text and buttons down. On product pages with many thumbnails this adds up to a red CLS.

Since Lighthouse 13 these checks have different names. According to Chrome for Developers, the unsized-images and layout-shifts audits were folded into the cls-culprits-insight, and prioritize-lcp-image and lcp-lazy-loaded into lcp-discovery-insight. If you search the current report for the old audit name from an older ticket or agency report, you will not find it. The cause has stayed the same.

In WordPress the errors usually come from:

  • Custom templates printing hard-coded <img src="..."> without attributes
  • Page builders storing image HTML without dimensions in post meta
  • SVG icons without a viewBox and without a CSS size
  • CSS background images, which cannot carry width/height in markup (reserve the space on the container)
  • Gallery shortcodes from older plugins rendering <img> without the attachment API
  • CDN URLs that mirror the file but lose the media library metadata

External images are a special case (hotlinked manufacturer assets or social CDNs). WordPress does not know their dimensions. Either host them locally and treat them as attachments, or protect the surrounding container with a fixed aspect ratio and min-height.

#How Google calculates the CLS score

Each individual shift gets a score from two factors: layout shift score = impact fraction × distance fraction. The impact fraction is the share of the viewport that the unstable element occupies across both frames. The distance fraction is the greatest distance it moved, relative to the larger viewport dimension. An element that, together with its new position, covers half the viewport and moves by 15% of the viewport height scores 0.5 × 0.15 = 0.075.

CLS no longer adds these values over the entire lifespan of the page. web.dev defines CLS as the largest burst of layout shift scores: shifts with less than one second between them form a session window, and a window lasts at most five seconds. The window with the highest total counts. A long page with many small shifts spread over time does not turn red automatically.

Two exceptions matter for theme developers. Shifts within 500 ms of a discrete input (tap, click, key press) carry the hadRecentInput flag and are excluded from CLS. Scrolls, drags and pinch-zoom do not count as such input. And animations using transform do not move other elements, so according to web.dev they do not count toward CLS.

Google judges field data (CrUX) more strictly than a single Lighthouse lab run. The 75th percentile of page loads decides, split by mobile and desktop. So after a fix, measure real page types (home, post, category, product, cart), not just the one URL that happened to turn green.

CLS (good / needs improvement / poor)Typical image cause
≤ 0.1Dimensions and aspect ratio set
0.1-0.25Mixed templates, late banners
> 0.25Hero without dimensions, ads without slots, fonts and images combined

Lab CLS and field CLS can diverge. Lab runs often lack consent banners, A/B tests and slow third-party scripts. If Search Console says “poor” and Lighthouse says “good”, trust the field data and open Layout Shift Regions on a real phone or a realistic emulation.

#What the browser does with an image that has no dimensions

When an image lacks dimensions, the browser lays it out at a height of 0 px. Once enough of the file has arrived for the decoder to read the size from the image header, the layout is invalidated and everything below the image is laid out again. That second layout is the visible jump.

#How browsers derive the aspect ratio from width and height

According to MDN, width and height are integers without a unit that give the intrinsic size in pixels. They do not decide how large the image is finally displayed. Their job: the browser computes the aspect ratio from them before the image has loaded. Current browsers set a value such as aspect-ratio: auto 640 / 360 internally (web.dev).

The auto in that value has a precise meaning. MDN describes it like this: for a replaced element such as <img>, the given ratio is used until the content has loaded, then the natural ratio of the file applies. If the attributes and the file do not match, the image still jumps after loading. That is exactly what happens when a template writes the original’s dimensions into the attributes but serves a differently cropped size.

Three rules follow:

  1. All candidates in one srcset need the same aspect ratio, otherwise the attributes are right for only one of them.
  2. With art direction in <picture> and different crops, each <source> gets its own width/height. According to web.dev, Chrome, Firefox and Safari now support that.
  3. Lazy-loaded images need the attributes most of all. MDN notes that an unloaded image without dimensions is 0 × 0 pixels and may therefore never intersect the visible area that would trigger its loading.

A widespread but ineffective rule is aspect-ratio: attr(width) / attr(height). Without a type, attr() returns a CSS string according to MDN, and a string is not a valid ratio. The declaration is dropped. The browser already does the derivation itself, so the rule can go.

#How to fix image CLS in the HTML markup

#1. Set width and height

<img src="photo.jpg" width="800" height="600" alt="Description">

In WordPress, prefer the core function that reads dimensions from the attachment metadata:

echo wp_get_attachment_image( $attachment_id, 'large' );

wp_get_attachment_image() writes width, height and, when sub-sizes exist, srcset/sizes. WordPress sets the class attachment-large size-large and loading itself; a manual 'loading' => 'lazy' is not needed, because since 6.3 core decides which images may be lazy-loaded. Hard-coded <img> tags in legacy themes are the most common reason why only new posts are “clean” while the archive keeps producing CLS.

If you use ACF image fields, store the attachment ID, not just the URL. From the ID you build wp_get_attachment_image(). From a bare URL you are left with guessing or an extra lookup against the attachment table.

#2. CSS aspect-ratio for containers and crops

For content images with correct attributes, a responsive base rule is enough. A fixed ratio on every content image would force portraits and panoramas into 4:3 and distort them:

.entry-content img {
  max-width: 100%;
  height: auto;
}

.hero-media img {
  aspect-ratio: 16 / 9;
  width: 100%;
  height: auto;
  object-fit: cover;
}

A fixed aspect-ratio belongs on components with a deliberate crop: hero, card grids, product tiles, video embeds and containers with a background image. Combine it with sensible sizes, otherwise the browser downloads oversized variants and LCP suffers alongside CLS.

Before aspect-ratio, themes reserved space with the padding-bottom hack: a wrapper with height: 0 and padding-bottom: 56.25% (9 divided by 16), with an absolutely positioned image inside. It still works but needs extra markup. If you inherit an old theme, move it to aspect-ratio during the next template refactor, not as a project of its own.

#3. Use lazy loading correctly

Lazy loading belongs below the fold. The LCP element (often the hero image) must not carry loading="lazy". Otherwise the browser delays the request until layout is known, which pushes LCP back. See our Core Web Vitals guide for the other LCP phases.

In a template, the most direct way is to mark the hero image as important explicitly:

echo wp_get_attachment_image(
	$hero_id,
	'full',
	false,
	array(
		'class'         => 'hero-media__img',
		'fetchpriority' => 'high',
	)
);

With fetchpriority="high", wp_get_loading_optimization_attributes() treats the image as in the viewport and omits loading="lazy". The once common approach through the wp_get_attachment_image_attributes filter, which switches loading to eager afterwards, has a catch since WordPress 6.7: by then core has already prepended auto to sizes on lazy images. According to the 6.7 dev note, browsers reject sizes="auto, ..." on an image that is not lazy-loaded and fall back to 100vw. The hero then downloads in its largest variant. If you must keep the filter, strip the prefix:

add_filter( 'wp_get_attachment_image_attributes', function ( $attr ) {
	if ( ! empty( $attr['class'] ) && false !== strpos( $attr['class'], 'skip-lazy' ) ) {
		$attr['loading']       = 'eager';
		$attr['fetchpriority'] = 'high';
		if ( isset( $attr['sizes'] ) ) {
			$attr['sizes'] = preg_replace( '/^\s*auto\s*,\s*/i', '', $attr['sizes'] );
		}
	}
	return $attr;
} );

According to MDN, fetchpriority is only a hint to the browser, with the values high, low and auto. Put high on exactly one image per page, otherwise several downloads compete for the same priority.

Shop themes often lazy-load the first product image because a generic filter marks every img. An exception for above-the-fold images saves CLS and LCP. Check sliders too: if slides 2 and 3 are lazy but slide 1 has no dimensions, the whole carousel frame still jumps.

#What WordPress has done automatically since 5.5, 6.3 and 6.7

Many tutorials recommend code for things core took over long ago. The state according to the official dev notes:

VersionChangeSource
4.4srcset and sizes for attachment imageswp_get_attachment_image
5.5loading="lazy" for images with width and height; missing dimensions are added to images with the wp-image-{ID} class5.5 dev note
6.1decoding attribute in wp_get_attachment_image()Function changelog
6.3fetchpriority="high" on the likely LCP image, threshold for non-lazy images raised from 1 to 36.3 dev note
6.7sizes="auto, ..." for lazy-loaded images6.7 dev note

The dimension back-fill runs in wp_filter_content_tags(), which default-filters.php hooks to the_content at priority 12, after shortcodes. Lazy loading skips the first three content images; the wp_omit_loading_attr_threshold filter controls the number. For fetchpriority="high", the 6.3 dev note requires an image area of at least 50,000 pixels, adjustable with wp_min_priority_img_pixels. A small logo therefore does not get the priority.

The practical consequence: images inserted through the block editor with a media library reference are rarely the problem in current versions. The errors sit in theme templates, builder output, hard-coded HTML and images without a wp-image-{ID} class.

#Which image sizes WordPress creates on upload

According to the WordPress source, a fresh install creates these sizes: thumbnail 150 px, medium 300 px, medium_large 768 px and large 1024 px (width in each case, adjustable in the settings). Since 5.3, 1536x1536 and 2048x2048 are added. Also since 5.3, WordPress scales originals whose longest side exceeds 2560 px down to that limit and saves them with the suffix -scaled; the big_image_size_threshold filter changes or disables the value.

For srcset, wp_calculate_image_srcset() only includes variants with the same aspect ratio and cuts off at 2048 px width by default (filter max_srcset_image_width). Theme sizes registered with add_image_size() and a hard crop (crop = true) have a different ratio from the original and therefore never appear in the original’s srcset. A card grid with a 4:3 crop needs several custom sizes in the same 4:3 ratio, otherwise there is only one file for every screen.

#How to use responsive images and srcset in WordPress

Core outputs srcset when you use the attachment APIs. Page builders and ACF image fields with a manual URL bypass it. Then both the dimensions and the matching variants are missing.

How the browser chooses: with w descriptors in srcset it ignores src and uses sizes to work out the expected display width. If sizes is missing, MDN says 100vw applies. That is why WordPress has set a default since 4.4 that follows the pattern (max-width: {width}px) 100vw, {width}px; themes are expected to adapt it to the real layout with the wp_calculate_image_sizes filter. Since 6.7 core prepends auto on lazy-loaded images so the browser uses the actual rendered width.

Checklist for editors and developers:

  1. Media library: do not upload originals 6000 px wide without a reason
  2. The theme registers sensible add_image_size variants in the same aspect ratio
  3. Templates never call only wp_get_attachment_url() for visible content images
  4. After a theme switch, regenerate sub-sizes (for example wp media regenerate), otherwise srcset candidates are missing
  5. Match the sizes attribute to the real layout width (sidebar vs full bleed)

A typical magazine case: desktop content is 720 px wide, mobile 100 vw. If sizes is 100vw, the desktop browser often downloads the 2048-pixel variant for a 720-pixel column. That is not a CLS bug, but it lengthens LCP and makes the page feel heavy. Think about CLS and LCP together, not one after the other.

#Image size smaller than recommended: Discover and Article markup requirements

The Search Console warning “image size smaller than recommended” has nothing to do with CLS. It concerns the image Google uses for previews: the featured image, og:image and the image field in Article structured data. The warning dates back to the AMP era; today’s requirements live in two Google documents.

For Google Discover, large images should be at least 1200 px wide, exceed 300,000 pixels in total and use a 16:9 ratio. Large previews appear only when the page allows max-image-preview:large or uses AMP. Without that directive Google shows small thumbnails. Do not give Google the site logo as og:image or in schema markup as the preview image.

The Article structured data documentation recommends several high-resolution images with at least 50,000 pixels (width multiplied by height) in the ratios 16:9, 4:3 and 1:1. Image URLs must be crawlable and indexable.

A check for editors that logs featured images that are too narrow on publish:

add_action( 'publish_post', function ( int $post_id ) {
	$thumbnail_id = get_post_thumbnail_id( $post_id );
	if ( ! $thumbnail_id ) {
		return;
	}
	$meta  = wp_get_attachment_metadata( $thumbnail_id );
	$width = (int) ( $meta['width'] ?? 0 );
	if ( $width < 1200 ) {
		error_log( sprintf( 'Post %d: featured image only %d px wide.', $post_id, $width ) );
	}
} );

For scaled uploads, wp_get_attachment_metadata() returns the dimensions of the -scaled file, so at most 2560 px. That is enough for Discover.

#How to avoid CLS in WooCommerce and grid layouts

Product grids are CLS hotspots. Every tile needs the same image frame. If a product has no featured image and the fallback is a narrower placeholder PNG, the card jumps. Set one wrapper with an aspect ratio and object-fit: cover for product images in the theme.

Variations and galleries: WooCommerce swaps thumbnails with JavaScript. Reserve the gallery height before the variation scripts fire. Otherwise someone clicks “size M”, the large image loads late and the add-to-cart button moves. Shifts within 500 ms of the click do not count toward CLS, a reload after that does, and the button moves away under the finger either way.

#How web fonts, banners and third-party scripts cause CLS

Not every CLS is an image problem. Cookie banners that jump from 0 to 120 px after consent, fonts without a suitable font-display strategy and late affiliate widgets create the same shift regions in DevTools. Reserve minimum heights for consent bars and ad slots before scripts load, wrapping ad slots in static containers with min-width and min-height that match the expected ad sizes. web.dev also advises against collapsing reserved space when no ad is served; show a placeholder instead.

In Chrome: open DevTools, then the command menu (Ctrl+Shift+P or Cmd+Shift+P), type Show Rendering and tick Layout Shift Regions. Reload, and blue areas mark the culprits. Watch mobile emulation; desktop CLS can be green while mobile CrUX stays red.

Other sources beyond images:

  • Web fonts that swap from fallback to custom (font-display: optional uses the font only if it is available at first layout, according to web.dev)
  • Embedded videos and iframes without an aspect-ratio box
  • Sticky headers that grow with scroll position
  • Live chat bubbles that appear only after idle
  • Animations on top, left or height instead of transform

#How to add missing width and height to old WordPress images

Images with the wp-image-{ID} class need no custom code: since 5.5, wp_filter_content_tags() adds missing width/height itself, using the dimensions of the size actually embedded. A filter that inserts the full dimensions instead writes wrong attributes for an embedded 300 px thumbnail.

Custom code only pays off for tags without that class, for example from imported content or old builder HTML. The following filter runs after core, finds the attachment ID from the URL and uses the dimensions of the matching size:

add_filter( 'the_content', function ( $content ) {
	if ( false === strpos( $content, '<img' ) ) {
		return $content;
	}

	return preg_replace_callback(
		'/<img\b[^>]*>/i',
		function ( $matches ) {
			$img = $matches[0];
			if ( preg_match( '/\swidth=/i', $img ) || ! preg_match( '/\ssrc="([^"]+)"/i', $img, $src ) ) {
				return $img;
			}
			// attachment_url_to_postid() only knows the original URL, so strip the size suffix.
			$original = preg_replace( '/-\d+x\d+(?=\.\w+$)/', '', strtok( $src[1], '?' ) );
			$id       = attachment_url_to_postid( $original );
			if ( ! $id ) {
				return $img;
			}
			$size = wp_image_src_get_dimensions( $src[1], wp_get_attachment_metadata( $id ), $id );
			if ( ! $size ) {
				return $img;
			}
			return str_replace( '<img', sprintf( '<img width="%d" height="%d"', $size[0], $size[1] ), $img );
		},
		$content
	);
}, 13 );

This is a safety net, not a replacement for clean templates. attachment_url_to_postid() runs one database query per image; for large archives, a one-off WP-CLI run that writes the attributes into post_content beats filtering on every request. Images from other domains are left alone: querying them at runtime with getimagesize() downloads the file from the remote server on every cache miss.

#How to measure and debug CLS after the fixes

Do not just rerun Lighthouse after each fix. A process that holds up in agency projects:

  1. Open the Core Web Vitals report in Search Console and note the affected URL groups
  2. Check one URL per template type with Layout Shift Regions
  3. Emulate a mid-range Android phone, not only a desktop
  4. Wait 28 days of field data after the deploy before reporting “done”, because CrUX evaluates a rolling 28-day window
  5. Regression: a new hero block or cookie CMP needs the same check

For your own field data, Google’s web-vitals library works well. It reports CLS with the same session-window logic as CrUX:

import { onCLS } from 'web-vitals';

onCLS( ( metric ) => {
	navigator.sendBeacon( '/vitals', JSON.stringify( {
		name: metric.name,
		value: metric.value,
		id: metric.id,
	} ) );
} );

To list images without dimensions across a set of URLs, a short Node.js script with cheerio is enough:

import * as cheerio from 'cheerio';

async function auditUrl( url ) {
	const html = await ( await fetch( url ) ).text();
	const $ = cheerio.load( html );
	$( 'img' ).each( ( i, el ) => {
		if ( ! $( el ).attr( 'width' ) || ! $( el ).attr( 'height' ) ) {
			console.warn( `Image without dimensions: ${ $( el ).attr( 'src' ) } on ${ url }` );
		}
	} );
}

It reads the server HTML only, so images injected later by JavaScript need a check in the browser.

Document which page types you measured. If only the home page is green and the single post stays red, the job is not finished. Editors and developers need a shared definition of “done”.

#Checklist for image dimension errors and CLS in WordPress

  • All visible content images come through the attachment APIs
  • Hero without loading="lazy", with fetchpriority="high" on exactly one image
  • No sizes="auto, ..." on images that are not lazy-loaded
  • Theme add_image_size covers grid, single and OG, each in a consistent ratio
  • Featured images at least 1200 px wide, max-image-preview:large set
  • Consent banners and ad slots have reserved height
  • CrUX and Search Console rechecked after deploy
  • No background heroes without an aspect-ratio container
  • Product grid fallbacks keep the same tile height

#Common image CLS mistakes in WordPress projects

  • Optimization plugin enabled, but the theme still renders hard-coded <img> tags
  • An AMP or “mobile theme” fork with different markup from desktop
  • WebP conversion replaces files but leaves stale srcset candidates behind
  • Editors paste full browser-window screenshots (3000×2000) without cropping
  • Page builder stores absolute pixel widths that break on mobile and are then corrected with JavaScript

#Conclusion

Image dimension errors are rarely a single plugin problem. They appear where templates bypass the attachment API, where the hero and lazy loading collide, and where field data is ignored. Pin down dimensions, aspect ratios and repeatable measurements, and you get stable layouts and fewer Search Console warnings, without PageSpeed cosmetics on a demo URL.

Need help optimizing your site’s Core Web Vitals? Our WordPress development team can audit your templates and media pipeline. Contact us to discuss your project requirements.

Next step

Turn the article into an actual implementation

This block strengthens internal linking and gives readers the most relevant next move instead of leaving them at a dead end.

Related cluster

Explore other WordPress services and knowledge base

Strengthen your business with professional technical support in key areas of the WordPress ecosystem.

What causes image dimension warnings and CLS?#
Missing width and height attributes in HTML source markup, containers without reserved aspect ratios, and lazy loading applied above-the-fold.
Will adding width and height break responsive styling?#
No, specifying HTML attributes provides the native aspect ratio. CSS (e.g., width 100% and height auto) scales the image responsively while maintaining the reserved space.
How can I identify layout shifts on mobile devices?#
Enable Layout Shift Regions in Chrome DevTools Rendering options, reload, and verify shifting boundaries highlighted in blue.
Can I fix existing blog posts programmatically?#
Since WordPress 5.5 core adds missing width and height to images with a wp-image-ID class. For other tags, a the_content filter can match the source URL to an attachment ID and add the dimensions of that size.
How do I debug layout shifts caused by dynamic ad injections?#
Dynamic ad placements that load without reserved container spaces are a major source of CLS. To prevent shifts, wrap the ad slots in static container elements with min-width and min-height constraints matching the expected ad sizes. This ensures the browser reserves the layout space before the ad script fetches the iframe.

Need an FAQ tailored to your industry and market? We can build one aligned with your business goals.

Let’s discuss

Related Articles