Fix Cumulative Layout Shift (CLS)
CLS is a layout-reservation problem, not a loading-speed problem. The page jumps because something arrived later than the layout assumed: an image with no reserved box, a font that reflowed the text, a banner that pushed content down. The fix is not "load faster"; it is "reserve the right space before the late thing arrives." With that framing, most CLS work is mechanical.
This page covers what CLS measures, how to find the element that owns the shift, and how to fix it, split between what a server-layer optimizer handles for you and what only your own CSS and markup can. The largest single cause, images without explicit dimensions, ModPageSpeed fixes automatically; the rest needs reserved space you have to write. To see your own numbers, analyze a page first.
What CLS measures
Cumulative Layout Shift is the sum of unexpected layout shifts during load, weighted by how much of the viewport moved and how far it moved. Each shift scores as impact fraction × distance fraction: the impact fraction is the share of the viewport the shifting element occupies across its before and after positions; the distance fraction is the greatest distance any unstable element moved divided by the viewport's largest dimension. Both terms are fractions, which is why CLS is unitless. Shifts are grouped into session windows, and the heaviest window sets the page's value, so one bad jump matters more than many tiny ones spread out. A shift within 500 ms of, and attributable to, a user input does not count: expanding an accordion you just clicked is fine; the page lurching on its own is not.
The number you are scored on is the field 75th-percentile CLS from CrUX: real Chrome users over the trailing window. Under 0.1 is good, 0.1–0.25 needs improvement, and 0.25 and up is poor. Lighthouse and PageSpeed Insights show a single lab run, and the two often disagree: the lab serves HTML faster than real users, so font swaps and lazy-image jumps fire in the field but never in the lab. When they conflict, use the field number, which is what Google ranks on.
A single lab run from PageSpeed Insights approximates CLS; the field number from real Chrome users is what Google ranks on.
Common causes
Images and embeds with no width and height
The most common cause, and the one Web Almanac surveys flag on a majority of pages. With no width/height attribute the browser allocates zero space, paints the surrounding text, then jumps the layout by exactly the rendered image height when the bytes arrive. Every paragraph below the image moves.
Web fonts that reflow text on swap
A font loaded with font-display: swap paints a fallback first, then swaps in the web font. Because the fallback's metrics (line-height, x-height, character width) differ, every text block re-flows by a few pixels in both axes when the swap fires. This is a browser-timing concern, so it shows up in the field even when the lab reports a clean 0.
Content injected above what the user is reading
JS-injected content, lightboxes, content-loader scripts, cookie banners, and ad slots that mount into the document flow push everything below them down. Anything inserted above the current scroll position after first paint records a shift for the full distance it displaces.
Stale cached HTML referencing mismatched image dimensions
An nginx proxy_cache or a CDN edge serves HTML that embeds old (or no) dimensions while the image at that URL has been re-edited to a new size. The reserved box and the actual image no longer agree, so the layout snaps when the real bytes load. This is worst when the HTML TTL is long and the image refresh path is faster than the HTML one.
Third-party iframes that start at zero height
YouTube, Twitter/X, and Instagram embeds render an iframe that begins at zero or a placeholder height and expands when its own content loads, shoving the page below it. The parent document has no way to know the final height in advance, so the space is never reserved.
How to fix CLS
Diagnose before you change anything: find the element that owns the shift
Server + appStart in the field, not the lab. Search Console → Core Web Vitals tells you which URL groups fail CLS on real users. Then reproduce it: run the URL through the analyzer or PageSpeed Insights and read the "Avoid large layout shifts" diagnostic, which lists the specific elements and their score contribution.
Confirm it locally in Chrome DevTools: Performance panel → settings cog → enable Web Vitals and Layout Shift Regions, throttle to Slow 4G, and reload. Each shift is a red rectangle; hover the markers in the Timings track to name the DOM node. For repeatable numbers, load the web-vitals library and log onCLS with attribution. Get three things before touching code: a baseline score, the element that owns most of it, and which cause it maps to.
Reserve image space automatically with server-injected dimensions
Server layerCLS is the metric ModPageSpeed moves most, and this is why. The 2.0 worker injects explicit width and height on <img> tags that lack them, using dimensions read from the cached image data, so the browser reserves the correct layout box before pixels arrive. It is enabled by default. On mod_pagespeed 1.15 the same result comes from insert_image_dimensions, with lazyload_images deferring offscreen images (which must still carry explicit width/height so their space stays reserved).
This runs below the CMS, so one configuration fixes every URL the origin serves: hand-written HTML, classic-editor content, page-builder markup, with no markup changes or build step. Pair it with a single site rule, img { max-width: 100%; height: auto; }, so the browser computes the aspect-ratio box from the inserted attributes. Test it on responsive templates first: writing fixed dimensions can interfere with some CSS-driven responsive layouts.
Stabilize early layout with inlined critical CSS
Server layerA render-blocking stylesheet chain leaves a window where the browser paints unstyled or partially-styled content, then re-lays it out when the CSS lands: a flash-of-unstyled-content shift on top of the font and image ones. ModPageSpeed's heuristic critical CSS extraction scans the HTML, matches selectors against the above-the-fold DOM, and injects the result as a <style> tag before </head>, with no headless browser required, so above-the-fold styling is available without waiting on an external stylesheet. On 1.15 this is prioritize_critical_css, which shrinks the FOUC window during which layout-affecting CSS and font swaps arrive.
Keep cached HTML and image dimensions in sync
Server + appIf a proxy or CDN serves HTML that references an image at the wrong size, the reserved box is wrong no matter who set it. Content-hash your asset URLs so a re-edited image gets a new URL the HTML must reference, which ModPageSpeed's extend_cache (1.15) does, and keep HTML short-cached while assets are long-cached and immutable. On a cache miss ModPageSpeed serves the origin immediately and regenerates variants out of the request path, so dimensions stay correct without stalling the response. URL PURGE removes all variants for a URL when you need to force a refresh.
Tame web-font swap shifts
ApplicationThe rewriter does not fix font-swap reflow; that is a browser concern. Self-host your fonts and switch font-display: swap to font-display: optional: the browser gives the web font a brief window to arrive, and if it misses, the fallback stays for the whole page lifetime, so no swap fires after first paint and CLS drops. Where you must keep swap, preload the font file and add size-adjust / @font-face metric overrides so the fallback occupies the same space as the web font. This lives entirely in your own CSS.
Reserve space for iframes and JS-injected content
ApplicationModPageSpeed cannot size a third-party embed (it does not know a YouTube iframe's intrinsic height) or content that JavaScript injects after the initial response; those need CSS-level reserved space you write. Wrap embeds in a fixed-ratio box: <div style="aspect-ratio: 16 / 9"> with the iframe at 100% width and height. Give ad slots a min-height matching the largest creative, pin cookie banners with position: fixed instead of letting them push content, and put aspect-ratio on any container whose child loads late. Never let a late element decide its own size at runtime.
Fix CLS on your platform
The same metric fails for different reasons on different stacks. Pick yours for the concrete diagnosis and fix.
CLS: common questions
- What is a good CLS score?
- Under 0.1 is good, 0.1 to 0.25 needs improvement, and 0.25 and up is poor. The score that matters is the 75th-percentile value from real Chrome users in the field (CrUX), not a single Lighthouse lab run. Meeting 0.1 at the 75th percentile means three of four visits see a stable layout.
- How do I fix CLS?
- Diagnose first: find the element that owns the shift in Search Console and the PageSpeed Insights "Avoid large layout shifts" diagnostic. Then reserve space for it. The biggest cause is images without dimensions: ModPageSpeed injects
widthandheightautomatically at the server layer, enabled by default, so the browser holds the box before the image loads. Fonts, iframes, and JavaScript-injected content need reserved space in your own CSS. - Why is my field CLS worse than my Lighthouse CLS?
- Because the lab serves HTML faster than real users do, so font swaps and lazy-image jumps fire in the field but not in the single lab run. A lab CLS of 0 with a failing field score is common and expected. Use the field number, which is what Google ranks on, and optimize toward it rather than over-tuning to the lab.
- Does setting image width and height actually fix CLS?
- For image-driven shifts, yes: it is the canonical fix. With explicit
widthandheight(plus aimg { max-width: 100%; height: auto; }rule) the browser computes the aspect-ratio box and reserves the slot before pixels arrive, so no jump occurs. ModPageSpeed does this automatically from cached image data for every<img>that lacks dimensions, so you do not have to audit markup by hand. - Can a server-side optimizer fix all of my CLS?
- No, and we are precise about that. It fixes the largest cause, unsized images, automatically, and inlined critical CSS stabilizes early layout. It does not fix shifts from third-party iframes (the rewriter does not know an embed's intrinsic height), web-font swap reflow (a browser concern), or content injected by JavaScript after the initial response. Those need CSS-level reserved space you write yourself.
- Do user-triggered layout changes count against CLS?
- No. A shift within 500 ms of, and attributable to, a user input is excluded: expanding an accordion or opening a menu you just clicked does not score. CLS only counts unexpected shifts the user did not cause. A shift that merely happens to land in that 500 ms window but was not triggered by the interaction still counts.
Measure, then fix
Start with a measurement. Run a page through the analyzer to see what is driving its CLS, then fix it at the server layer with ModPageSpeed — it optimizes immediately, licensed or not. Production use requires a commercial license — but the software never locks you out.
See also:
- Core Web Vitals: the hub — how CLS fits with LCP and INP, and per-platform fix guides
- Analyze a page — see what is driving your CLS before you change anything
- Critical CSS without a headless browser — the heuristic behind the early-layout stabilization above
- ModPageSpeed 2.0 features — server-injected image dimensions, critical CSS, and variant-aware caching