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.
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
- Static HTML placeholders with reserved dimensions
requestIdleCallbackorloadevent for script injection- 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.