NNicheStackTutorials
← All tutorials
SEO & Web Vitals

Lazy-Load AdSense Without Hurting LCP on Static Blogs

Intersection Observer + reserved ad slots kept LCP at 1.0s on my Hugo blog. Copy-paste snippet for delayed AdSense load after first paint.

·12 min read·SEO & Web Vitals
Lazy-Load AdSense Without Hurting LCP on Static Blogs cover

Problem: Loading AdSense in <head> pushed LCP from 1.1s to 3.2s. Removing ads fixed LCP but killed revenue. Goal: Ads load after LCP, without CLS, without policy violations.

TL;DR

  • Don't load AdSense until after first contentful paint.
  • Use Intersection Observer for below-fold slots only.
  • Always reserve min-height — lazy load ≠ zero layout space.

What Google actually measures for LCP

LCP is the largest paint in the viewport during load. Sidebar ads below the fold rarely become LCP. In-article ads above the first paragraph often do — or they delay the real LCP (hero image) by competing for bandwidth.

Rule: Never load ad scripts before LCP candidate finishes painting.

Architecture that worked

  1. Static HTML placeholders with reserved dimensions
  2. requestIdleCallback or load event for script injection
  3. Intersection Observer for below-fold units only
<div class="ad-slot" data-ad-slot="sidebar" style="min-height:250px;min-width:300px"></div>
function loadAdSense() {
  if (window.__adsLoaded) return;
  window.__adsLoaded = true;
  const s = document.createElement('script');
  s.src = 'https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-XXXX';
  s.async = true;
  s.crossOrigin = 'anonymous';
  document.head.appendChild(s);
  s.onload = () => document.querySelectorAll('.ad-slot').forEach(initSlot);
}

if ('requestIdleCallback' in window) {
  requestIdleCallback(loadAdSense, { timeout: 2500 });
} else {
  window.addEventListener('load', () => setTimeout(loadAdSense, 1500));
}

Below-fold: Intersection Observer

const observer = new IntersectionObserver((entries) => {
  entries.forEach((e) => {
    if (e.isIntersecting) {
      initSlot(e.target);
      observer.unobserve(e.target);
    }
  });
}, { rootMargin: '200px' });

document.querySelectorAll('.ad-slot[data-lazy]').forEach((el) => observer.observe(el));

Mark in-article ads below paragraph 3 with data-lazy. Sidebar gets eager init after idle — it's visible on desktop but not LCP.

initSlot without layout shift

function initSlot(el) {
  if (el.dataset.initialized) return;
  el.dataset.initialized = '1';
  el.innerHTML = '<ins class="adsbygoogle" style="display:block" data-ad-client="ca-pub-XXXX" data-ad-slot="YYYY" data-ad-format="auto" data-full-width-responsive="true"></ins>';
  (adsbygoogle = window.adsbygoogle || []).push({});
}

Parent min-height stays. Ad may not fill — acceptable CLS if min-height matches typical fill rate.

Hugo integration

layouts/partials/ads/lazy-loader.html — include once in baseof.html before </body>.

Shortcode for in-article:

<div class="ad-slot ad-inarticle" data-ad-slot="in-article" data-lazy
     style="min-height:280px;margin:1.5rem 0"></div>

Astro: client-only ad component

---
// AdSlot.astro — no frontmatter JS on server
---
<div class="ad-slot" data-lazy style="min-height:280px" data-ad-slot="in-article"></div>
<script is:inline src="/scripts/lazy-ads.js"></script>

Use is:inline for the loader — don't bundle with Vite (adds to main chunk).

What failed

  • Native loading="lazy" on iframes: AdSense injects iframes; you don't control them.
  • Deferring until scroll: Policy gray area; also hurts viewability RPM.
  • No min-height: CLS 0.24 even with lazy load.

Metrics

Before (sync head script): LCP 3.2s, RPM $4.10
After (idle + IO):          LCP 1.0s, RPM $3.85 (-6%)
After + 2nd in-article:     LCP 1.1s, RPM $4.55 (+11% net)

6% RPM dip was worth it — rankings recovered and organic sessions rose. Faster site = more pageviews = more ad impressions.

Policy note

AdSense allows async loading. Don't hide ads, don't click-bait load on accidental hover. Lazy below-fold is standard industry practice.

FAQ

Does lazy loading ads improve LCP?

Yes — deferring ad scripts prevents them from competing with LCP resources during initial load.

Should I lazy load above-the-fold ads?

No — above-fold slots should load soon after first paint but not block LCP. Below-fold: use Intersection Observer.

Will delayed AdSense hurt fill rate?

Minimal impact if delay is 2–3 seconds or post-paint. One test showed 94% fill rate retained with 2.5s delay.

Tags: Lazy LoadingAdSenseLCPIntersection Observer