Fix Layout Shift From Popunder and Banner Ads on Static Sites
CLS 0.31 → 0.03 with reserved zones, sticky footer slot, and popunder config that doesn't inject top banners.
Network: Adsterra banner + popunder on Hugo tutorial blog.
Symptom: CLS 0.31, Lighthouse "Avoid large layout shifts" pointing at injected iframe above content.
Root cause: Network's auto banner injected a 728×90 at top of <body> with no reserved space.
Diagnose in Performance panel
Chrome DevTools → Performance → check "Experience" row → Layout Shift entries.
Click shift — see which node moved. Mine: main.content pushed down 90px when banner appeared.
Fix 1: Reserved top banner zone (if you must use top banners)
<body>
<div id="top-ad-zone" class="ad-zone-top" aria-hidden="true"></div>
<header>...</header>
<main>...</main>
</body>
.ad-zone-top {
min-height: 90px;
max-height: 90px;
overflow: hidden;
text-align: center;
}
@media (max-width: 728px) {
.ad-zone-top { min-height: 50px; max-height: 50px; }
}
Configure network to target #top-ad-zone instead of auto-inject.
Fix 2: Sticky footer banner (better UX, lower CLS)
.ad-zone-footer {
position: fixed;
bottom: 0; left: 0; right: 0;
height: 50px;
z-index: 100;
background: #f8fafc;
}
body { padding-bottom: 50px; }
Fixed position doesn't cause CLS — it's out of document flow. padding-bottom on body reserves space from first paint.
Fix 3: Popunder settings
In Adsterra / Monetag panel:
- Disable "Banner before popunder" if offered
- Set frequency cap (1 per session)
- Exclude mobile if CLS critical (mobile ad revenue was 40% lower anyway)
Popunders themselves don't cause CLS — they're new windows. Companion banners do.
Fix 4: MutationObserver guard (last resort)
const observer = new MutationObserver((mutations) => {
mutations.forEach((m) => {
m.addedNodes.forEach((node) => {
if (node.tagName === 'IFRAME' && !node.closest('.ad-zone-top, .ad-slot')) {
node.style.cssText = 'position:fixed;bottom:0;left:0;z-index:99;';
}
});
});
});
observer.observe(document.body, { childList: true, subtree: true });
Nuclear option — repositions rogue iframes. Some networks ToS may restrict DOM manipulation. Check your contract.
Sidebar sticky ads and CLS
Sticky sidebar ad that changes height on scroll caused micro-shifts (CLS 0.05).
Fixed with:
.sidebar-ad { height: 600px; overflow: hidden; position: sticky; top: 80px; }
Test protocol
1. Lighthouse mobile, Slow 4G, 4x CPU throttle
2. Repeat 5 runs — CLS varies with ad fill
3. Test with ad blocker OFF (obviously)
4. Test logged-out, no consent banner dismissed mid-load
Before / after
CLS: 0.31 → 0.03
Bounce rate: 68% → 54% (mobile)
RPM: $2.10 → $2.35 (better viewability scores)
Google rewards stable layouts. Ad networks reward viewability. Reserved zones serve both.