NNicheStackTutorials
← All tutorials
SEO & Web Vitals

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.

·12 min read·SEO & Web Vitals
Fix Layout Shift From Popunder and Banner Ads on Static Sites cover

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.

Tags: CLSPopunderBanner AdsLayout Shift