Fastest fix: stop letting JavaScript create ad slots after the page paints. Put a real <div> placeholder with reserved min-height (and min-width) into your HTML at every ad position, drop the <ins class="adsbygoogle"> inside it, and turn off the matching Auto Ads format so the two do not double up. When AdSense fills a slot that already occupies space, only the iframe content changes and CLS stays near zero.
PageSpeed used to report CLS 0.04. After you turn on Auto Ads — or after a developer adds a “smart” ad-injection script that scans the DOM and inserts slots between paragraphs — CLS jumps to 0.28 and Search Console flags Core Web Vitals as “Poor” on every article URL. The visual symptom is unmistakable: a reader is halfway through the third paragraph when the page jolts down ~250px because an ad finally injected itself above their scroll position. This is not the classic “I forgot min-height on my <ins> tag.” The slots do not exist in the HTML at all when LCP fires — they appear later, via JavaScript, and every appearance is a layout shift.
As of June 2026, Google’s CLS thresholds are unchanged: <= 0.1 is “Good”, > 0.25 is “Poor”, and anything between needs improvement. Crucially, the score that matters is the 75th-percentile of real visits (field data) over a rolling 28-day window, segmented separately for mobile and desktop — not a single lab run. Google’s own optimization guide names ads as “one of the largest contributors to layout shifts on the web,” so this is a known, fixable pattern, not a quirk of your theme.
Which bucket are you in?
| Symptom you observe | Most likely cause | Jump to |
|---|---|---|
<ins> tags appear between paragraphs that were not in your source HTML | Auto Ads in-article injection | Cause 1 |
Theme code calls insertAdjacentHTML / appendChild near adsbygoogle | Custom injection script, no skeleton | Cause 2 |
| Slot reserved 280px but DevTools shows it rendered taller | Creative larger than reserved height | Cause 3 |
<body> gains padding-bottom after ads load | Anchor ad pushing instead of overlaying | Cause 4 |
| Slots appear only after the consent click | Late CMP re-rendering the ad region | Cause 5 |
| Shift fires mid-scroll as a below-fold slot enters view | Lazy slot with no reserved space | Cause 6 |
Common causes
Ordered by how often we see them in real audits.
1. Auto Ads inserting in-article slots after first paint
Auto Ads’ in-article placement scans your DOM in the browser, picks paragraph breaks, and injects <ins> tags via JavaScript. Even when the slot has reserved height, the act of inserting a new block element pushes everything below it down — a layout shift the size of the slot.
How to spot it: Open the page, wait for ads to load, then view the rendered HTML. If <ins class="adsbygoogle"> tags appear between paragraphs that were not in your source HTML, Auto Ads injected them.
2. Custom ad-injection script with no skeleton placeholder
Many CMS themes ship a “smart” script that walks article content, finds every Nth paragraph, and inserts an ad after it. Without a pre-rendered skeleton at that position, each insertion is a fresh layout shift.
How to spot it: Search your theme for insertBefore, insertAdjacentHTML, or appendChild near a string like adsbygoogle. If you find it, that is the source.
3. Responsive ad unit picks a taller creative than the reserved height
You set min-height: 280px for a slot. AdSense serves a 336x600 creative. The slot expands by 320px, shifting all content below. Lighthouse counts this as a CLS event because the shift happens after the initial render window. Google’s Publisher Tag guidance is explicit here: for a slot that can serve multiple sizes, reserve space for the size that fills most often (check your historical fill data) or for the largest configured size — never the smallest.
How to spot it: DevTools → Performance → record a load. Look for layout shift entries whose node is <ins class="adsbygoogle"> with a value above 0.05. If the affected node had a height smaller than the served ad, this is the cause.
4. Anchor ad or vignette ad covering content with no shift budget
Anchor (sticky bottom) ads should not cause CLS because they overlay rather than push content. But if the anchor ad implementation prepends a padding-bottom to <body> after load, that padding addition is a shift.
How to spot it: Inspect <body> styles before and after ads load. If padding-bottom changed from 0 to 90px, the anchor unit is shifting layout instead of overlaying.
5. Late-loading consent banner re-rendering the ad region
A CMP banner appears at second 1.0, the user clicks accept at second 2.5, and the page then injects ad slots that did not exist before consent. Every post-consent insertion counts as CLS because it happens well after the page is interactive.
How to spot it: Time-travel through the DevTools Performance recording. If ad slots appear only after the consent click, the CMP timing is the cause.
6. Lazy-loaded ad firing exactly as user starts scrolling
Lazy ads are loaded as they enter the viewport. If the slot has no reserved height and the user is mid-scroll when it triggers, the shift happens inside the user’s reading focus — Lighthouse weights this as a high-impact CLS.
How to spot it: Search Console → Core Web Vitals → “CLS issue” sample URL. The report shows screenshots before/after the shift. If the shifted element is a lazy slot, this is it.
Before you start
- Confirm the CLS regression is real by sampling 5 article URLs in PageSpeed and CrUX; do not trust a single Lighthouse run.
- Snapshot current PageSpeed scores so you can measure the fix delta.
- Record whether you use Auto Ads, manual units, or both — the fix path differs.
- Note your CMP vendor and consent flow timing.
Information to collect
- PageSpeed Insights “Layout Shift Elements” diagnostic — names every shifting node.
- DevTools Performance recording with “Web Vitals” overlay enabled.
- Your raw HTML (view-source) vs rendered DOM diff at the ad-slot insertion points.
- Auto Ads density setting in AdSense → Ads → By site → Edit.
- Any custom JS that touches the article DOM after
DOMContentLoaded.
Step-by-step fix
Ordered by impact and ease.
Step 1: Pre-render skeleton placeholders for every ad position
Replace runtime injection with HTML that already exists at first paint:
<article>
<p>First paragraph...</p>
<p>Second paragraph...</p>
<div class="ad-slot" data-position="in-article-1" style="min-height: 280px; width: 100%;">
<ins class="adsbygoogle"
style="display:block; text-align:center;"
data-ad-layout="in-article"
data-ad-format="fluid"
data-ad-client="ca-pub-XXXX"
data-ad-slot="YYYY"></ins>
</div>
<p>Third paragraph...</p>
</article>
The <div> exists in HTML at first paint with reserved height. When AdSense fills it later, no new element is inserted — only its <iframe> content changes — and CLS stays near zero.
One subtlety for data-ad-format="fluid" in-article units: AdSense needs to measure the slot’s width to pick a layout, so give the wrapper a real min-width too (the width: 100% above gives it that). A fluid slot that ends up zero-width can serve blank, which is its own problem — see AdSense script installed but blank.
Do not collapse the placeholder when no ad fills it. Google’s own CLS guide warns that “removing the space set aside for elements can cause just as much CLS as inserting content.” Leave the reserved box in place; a little whitespace beats a shift.
Step 2: Disable Auto Ads in-article placement if you have manual slots
In AdSense → Ads → By site → your domain → Edit → Auto ads:
- Turn OFF “In-article ads”
- Turn OFF “In-feed ads”
- Keep “Anchor” only if you confirmed it overlays without pushing
Auto Ads and manual slots double-up in practice. Pick one strategy. If you want the simplicity of Auto Ads, set density to Low and rely on its sidebar / anchor placements only. For the full tradeoff between the two approaches, see AdSense auto vs manual.
Step 3: Reserve generous height per slot
For responsive slots, reserve the LARGEST plausible serve size, not the average:
.ad-slot[data-position="in-article-1"] { min-height: 336px; }
.ad-slot[data-position="sidebar"] { min-height: 600px; }
.ad-slot[data-position="header"] { min-height: 90px; }
Yes, this leaves visible whitespace if a smaller creative serves. That whitespace is much cheaper than a CLS hit. Aim for min-height equal to your 90th-percentile served creative (pull the size distribution from AdSense reporting, not a guess). Note that mobile and desktop serve different sizes, so set device-specific reservations with media queries; see AdSense mobile vs desktop fill rate for why the two diverge.
Reserve with CSS, never with JavaScript. Setting a height in a script that runs after load just moves the shift later — it does not remove it.
Step 4: Move CMP decision before render, not after
If your CMP injects after page load, switch to a “render-blocking-then-async” pattern:
<head>
<script src="https://your-cmp.example/cmp.js" data-mode="prerender"></script>
<!-- CMP must call back before adsbygoogle.js loads -->
</head>
Most CMPs (Google Funding Choices, OneTrust, Cookiebot) have a “stub mode” that reserves consent decision space in HTML and resolves it without DOM mutation. Enable it.
Step 5: Switch anchor ad to overlay positioning
If your anchor unit shifts layout, fix the wrapper:
.adsense-anchor-wrapper {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 9999;
/* DO NOT add padding-bottom to body */
}
The anchor floats on top. Do not pad the body. If users complain content is hidden behind the anchor, ensure the unit itself has a close (X) button — AdSense’s default does.
Step 6: Lazy-load with a tall placeholder, not zero height
When using loading="lazy" on slot containers, the placeholder must still reserve space:
<div class="ad-slot lazy" style="min-height: 280px;">
<ins class="adsbygoogle" loading="lazy" ...></ins>
</div>
Without that min-height, the lazy slot is a 0-height target until the user scrolls onto it — and then it expands inside the viewport. CLS catastrophe.
Truly fluid slots cannot be pre-sized exactly. Google’s guidance is to put fluid units below the fold only and fetch them as early as possible, so any residual resize happens off-screen rather than under the reader’s eyes.
Step 7: Re-measure on a real device
PageSpeed runs in a clean Chrome profile with no extensions. Real users have ad-blockers (less ad shift) or trackers (more shift). Test on:
# Lighthouse CLI on your URL
npx lighthouse https://your-site.example/article-slug --view --preset=desktop
npx lighthouse https://your-site.example/article-slug --view --preset=mobile
Target CLS <= 0.1 on both presets.
How to confirm it’s fixed
- Lab check (instant): PageSpeed Insights shows CLS
<= 0.1for the previously-flagged URLs, and the “Layout Shift Elements” diagnostic no longer lists an<ins class="adsbygoogle">node. - DevTools (instant): Performance recording shows no
LayoutShiftentry above 0.05 after the LCP timestamp. - Manual: Scroll through an article on throttled “Slow 4G” — no visible jump after first paint, even as ads fill.
- Field check (the one Google scores): In Search Console → Core Web Vitals, open the CLS issue and click Validate Fix. Google re-checks against CrUX field data over a 28-day rolling window; the “Poor (CLS)” URL count drops to zero only after enough real-user data accrues. Lab passes do not move this number — only field data does.
Long-term prevention
- Treat every ad slot as a first-class HTML element with reserved height — never inject from JS.
- Pick ONE ad strategy (Auto Ads OR manual) per page template; never mix on the same template.
- Reserve the 90th-percentile served size, not the average — better to leave whitespace than to shift.
- Run Lighthouse in CI on a sample article URL; fail the build if CLS regresses above your threshold.
- Audit any new “smart ad placement” plugin against this checklist before installing.
- Monitor Search Console Core Web Vitals monthly; CLS regressions show up within 14-28 days.
Common pitfalls
- Reserving
min-height: 50pxfor an in-article slot that often serves a 250px creative — guarantees 200px of shift on every visit. - Disabling Auto Ads in dashboard but forgetting to remove the
data-ad-format="auto"attribute on a manual slot — they keep running. - Testing only on desktop while CLS regression is mobile-only (different ad sizes serve per device).
- Reading Lighthouse from one URL and assuming the rest of the site is fine — CLS varies by article length and ad density.
- Adding
padding-bottom: 90pxto<body>to accommodate an anchor ad, which is itself a layout shift.
FAQ
Q: I reserved height but CLS is still 0.18 — what gives?
The reserved height likely does not match the served creative size. Open DevTools, inspect the slot after load, and compare its rendered height to your reserved min-height. If the rendered height is larger, raise your reservation to match. See also AdSense slow page.
Q: Can I just hide the slot until it has loaded?
No — that creates an even bigger shift when it appears. The fix is reserved space, not delayed display.
Q: Does turning off Auto Ads kill my revenue?
In our experience manual placement with reserved space typically earns somewhat less RPM than full Auto Ads, often in the rough range of 5-15% (it varies by niche and layout). You trade that for a CWV pass. If your pages are demoted in search for poor CWV, the traffic loss usually dwarfs the RPM gap. See Auto Ads poor placement and AdSense auto vs manual for the tradeoff in detail.
Q: My CMP is the actual cause — how do I prove it?
Disable the CMP entirely in a staging environment, re-run Lighthouse. If CLS drops under 0.05, the CMP is your shift source. Ask the vendor about a “stub” or pre-rendered consent UI that reserves decision space in HTML, or switch CMP.
Q: PageSpeed says CLS is 0.05 but Search Console still says “Poor” — why?
PageSpeed shows a lab run from one device; Search Console shows the 75th-percentile of real visits over the last 28 days. Until enough good field data accumulates, the field grade lags your lab fix. Keep the fix deployed, click Validate Fix, and wait out the 28-day window. They will converge.
Q: Will Search Console penalize me for high CLS?
Indirectly. Core Web Vitals is a ranking signal and “Poor” pages can be demoted. Recovery follows 28 days of passing CrUX field data, not lab data. See thin pages AdSense review for adjacent quality-signal concerns.
Tags: #AdSense #Monetization #cls #Core Web Vitals #Troubleshooting