Skip to content
What To Fix First

How to fix layout shift in WordPress

Layout shift on WordPress comes from a short list: images the theme outputs without width and height, Google Fonts swapping in late, a cookie or notice bar injected above the content, ad and embed plugins with no reserved height, and a lazy-loaded logo. Each has a specific plugin setting or a few lines of code, listed in order.

By , who built this checker and writes the guides. Published How the tool ranks fixes

Find the element that moves

Run the WordPress speed test on the page: when CLS is over 0.25 the headline names the layout fix, and the opportunity list names the elements Lighthouse saw move. For every shift rather than the worst one, the CLS guide has a PerformanceObserver snippet to paste in the console. What follows is the WordPress version of each cause, with the setting that fixes it.

1. Images without width and height

WordPress 5.5 and later write width and height on images in post content, so the browser reserves the right box before the file arrives. The shifts come from the images it does not handle: theme template images, builder widgets that output their own markup, shortcode galleries, and any image whose original file the library could not measure.

  • Optimisation plugin toggle. WP Rocket: Media → “Add missing image dimensions”. Perfmatters: Assets → “Add Missing Image Dimensions”. LiteSpeed Cache: Page Optimization → Media → “Add Missing Sizes”. All three scan the HTML on output and add the attributes; enable one, not several.
  • Theme templates. Where the theme calls the_post_thumbnail() or wp_get_attachment_image(), the attributes are included. Where it prints <img src="..."> by hand, add them. Search the theme for <img.
  • CSS to make the attributes work with responsive layouts:
img { max-width: 100%; height: auto; }   /* the attributes set the ratio, CSS the display size */
.wp-block-image img, .elementor-image img { aspect-ratio: attr(width) / attr(height); }

Background images set in CSS never shift on their own, but the box they are in does if its height depends on content that arrives late. Give hero sections a min-height.

2. Google Fonts swapping in late

A theme that loads fonts from Google renders text in a fallback, then reflows every line when the real font arrives. Two settings and one file:

  • Self-host the font files. Perfmatters: Fonts → “Local Google Fonts”. The OMGF plugin does the same job on its own. Elementor: Settings → Advanced → “Google Fonts Load” set to swap, and the Pro version’s local hosting option. Astra, GeneratePress and Kadence each have a “load Google Fonts locally” option in the customizer.
  • font-display: swap so text shows immediately, then size-adjust on a fallback face so the swap moves nothing:
@font-face {
  font-family: "Body Fallback";
  src: local("Arial");
  size-adjust: 104%;         /* tune until the line lengths match */
  ascent-override: 92%;
  descent-override: 24%;
}
body { font-family: "Inter", "Body Fallback", sans-serif; }

The Google Fonts guide covers the render-blocking side of the same stylesheet.

3. Bars injected above the content

Cookie consent bars, “free shipping over £50” strips, announcement bars from a plugin, WooCommerce’s store notice. Any of them inserted at the top of the page after first paint pushes everything down.

  • Consent plugins (Complianz, CookieYes, Cookie Notice, GDPR Cookie Consent): set the banner position to bottom or a floating box. Both render with position: fixed, which takes no space in the flow.
  • Announcement bar plugins and theme top bars: if the bar must be at the top, render it in the initial HTML (most theme-native bars do) rather than injecting it with JavaScript, or reserve its height:
.site-header { padding-top: 40px; }         /* the bar's height */
.announcement-bar { position: absolute; top: 0; height: 40px; }
  • WooCommerce store notice: it is fixed-position by default in most themes; if yours puts it in the flow, the CSS above applies.

4. Ads and embeds with no reserved height

Ad slots are the largest source of CLS on content sites, because their height is unknown until the creative arrives and sometimes nothing arrives at all.

  • AdSense Auto ads insert units wherever the algorithm likes, including above content. In the AdSense dashboard, turn off “anchor” and “vignette” formats if they are shifting the page, or switch to manual units with reserved sizes.
  • Ad Inserter, Advanced Ads and similar: wrap each placement in a container with a min-height equal to the largest size the slot can serve, and never collapse an unfilled slot to zero:
.ad-slot-leaderboard { min-height: 90px; }   /* 728x90 / 320x50: use the tallest */
.ad-slot-rectangle   { min-height: 250px; }
  • YouTube, Twitter/X, Instagram embeds in post content: the block editor’s embed block reserves a 16:9 box for video, but social embeds size themselves after loading. Wrap them in a container with a min-height, or replace with a static image that links out.
  • Related-posts and newsletter plugins that inject a block into the content after load. Set them to render server-side, or reserve their height.

5. The lazy-loaded logo and header

An optimisation plugin that lazy-loads every image includes the logo, so the header renders short, then grows when the logo arrives. Exclude the logo and anything else in the header from lazy-loading (the same exclusion settings as in the failed-assessment guide), and give the logo explicit dimensions. A sticky header that changes height on scroll — a shrinking logo, a hidden top bar — is a shift on every scroll; keep the height constant and animate with transform.

6. Sliders and builder animations

A slider that initialises after load, or a builder section with an “entrance animation” that animates height or margin, is a shift generator. Slider Revolution and Smart Slider: set a fixed height for the slider container in the module settings. Elementor and Divi: choose animations that use transform and opacity (fade, slide-in via transform) and avoid the ones that grow elements. Or, as with LCP, replace the homepage slider with one static image.

Check it worked

Re-run the speed test. The lab CLS in the metrics table should be under 0.1 and the layout fix should no longer be the headline. The field number moves over the next 28 days; the lab number moves now. If the lab number is clean and the field number is not, the shift is happening after an interaction — a scroll, a tap, a consent click — which the lab never does; record a session in DevTools with the Experience track on, as the CLS guide describes.

Common questions

WordPress adds width and height automatically. Why are my images still shifting?
WordPress 5.5 added the attributes to images in post content that it can measure. It does not touch images a theme or builder outputs through its own template, background images set in CSS, images inserted by shortcodes, or an image whose file the media library could not read. Check the rendered HTML: an <img> with no width and height attributes is one WordPress did not get to.
Does a caching plugin fix CLS?
No. Caching changes how fast the same HTML arrives, not what is in it. A page that shifts uncached shifts cached. Some optimisation plugins have a 'add missing image dimensions' option (WP Rocket, Perfmatters, LiteSpeed) which does help, because it edits the HTML.
My CLS is fine on desktop and bad on mobile.
The mobile layout stacks things that sat side by side, so a late-arriving element pushes everything below it further. Test with a phone-sized viewport and an empty cache; the checker tests mobile only for this reason.
Is it the cookie banner?
Often. A banner injected at the top of the page after the first paint pushes the whole page down; one that appears as a fixed overlay at the bottom shifts nothing. Most consent plugins have a position setting. 'Bottom' or 'floating' with position: fixed is the one you want.
Which plugin can I install to fix it?
There is no plugin that fixes layout shift, because it is a markup problem specific to your theme and content. Optimisation plugins can add image dimensions and set font-display; the rest — reserved space for ads, embeds and bars — is a few lines of CSS in the theme. This page has them.

Check a page against this

Runs the same Core Web Vitals test as the WordPress speed test.

Free, no signup, no ads. We keep nothing except the shareable result, for 30 days.

Enter any public URL. You’ll get the one change that recovers the most load time, what it’s worth, and the measurements underneath.

Related guides

All guides