Cap. 17 · Ferramenta
Carregamento sob demanda de anúncios
Demonstração ao vivo do Cap. 17: os slots abaixo só carregam quando entram no campo de visão. Role a página e veja o contador subir. Abaixo, o código completo.
Ferramenta ao vivo
Demonstração ao vivo
Role para baixo ↓
Slots carregados0/6
Slot de anúncio #1 — aguardando viewport…#1 — carregado ✓
Slot de anúncio #2 — aguardando viewport…#2 — carregado ✓
Slot de anúncio #3 — aguardando viewport…#3 — carregado ✓
Slot de anúncio #4 — aguardando viewport…#4 — carregado ✓
Slot de anúncio #5 — aguardando viewport…#5 — carregado ✓
Slot de anúncio #6 — aguardando viewport…#6 — carregado ✓
Código-fonte
Código do módulo
IntersectionObserver com tratamento de erro + helper de preconnect. Copie ou baixe o .zip.
/** * lazyAds.js — Cap. 17: "Aparecer ou Sumir" * * Lazy loader for ad slots based on IntersectionObserver. Ads are one of the * heaviest things on a page and hurt Core Web Vitals; loading them only when * they are about to enter the viewport keeps the initial render fast while * still monetising below-the-fold inventory. * * Markup contract — mark each ad container with `data-ad-slot`: * * <div data-ad-slot data-ad-format="script" * data-ad-src="https://ads.example.com/tag.js"></div> * * <div data-ad-slot data-ad-format="iframe" * data-ad-src="https://ads.example.com/unit.html" * data-ad-width="300" data-ad-height="250"></div> * * <div data-ad-slot data-ad-format="callback" data-ad-id="sidebar-1"></div> * * Supported formats: * - "script" : injects a <script src="data-ad-src"> into the slot. * - "iframe" : injects an <iframe src="data-ad-src"> into the slot. * - "callback" : calls options.render(slot, slotData) so you can hand off to * a third-party SDK (e.g. googletag.display, Prebid, etc.). * * Everything is wrapped in try/catch and per-element error handling so one * broken slot never breaks the others. *//** * @typedef {Object} LazyAdsOptions * @property {string} [selector] CSS selector for ad slots. Default "[data-ad-slot]". * @property {string} [rootMargin] IntersectionObserver rootMargin. Default "200px". * @property {number} [threshold] IntersectionObserver threshold. Default 0. * @property {Element} [root] Optional scroll root. Default null (viewport). * @property {(slot: Element, data: Object) => void} [render] * Callback used for slots with data-ad-format="callback". * @property {(slot: Element, data: Object) => void} [onLoad] Called after a slot loads. * @property {(slot: Element, error: Error) => void} [onError] Called when a slot fails. *//** Sensible defaults, merged with whatever the caller passes in. */const DEFAULT_OPTIONS = { selector: "[data-ad-slot]", rootMargin: "200px", // start loading ~200px before the slot is visible threshold: 0, root: null, render: undefined, onLoad: undefined, onError: undefined,};/** * Reads the `data-ad-*` attributes off a slot element into a plain object. * @param {Element} slot */function readSlotData(slot) { const ds = slot.dataset || {}; return { format: ds.adFormat || "script", src: ds.adSrc || "", id: ds.adId || "", width: ds.adWidth || "", height: ds.adHeight || "", // Everything else in the dataset is passed through for callbacks. dataset: ds, };}/** * Marks a slot's loading state via a data attribute and CSS-friendly class, * so you can style loading / loaded / failed states. * @param {Element} slot * @param {"loading"|"loaded"|"failed"} state */function setState(slot, state) { slot.setAttribute("data-ad-state", state);}/** * Loads a "script" format ad by injecting a <script> tag into the slot. * Resolves/rejects via the provided callbacks (onerror handling included). * @param {Element} slot * @param {Object} data * @param {LazyAdsOptions} options */function loadScript(slot, data, options) { if (!data.src) { throw new Error("lazyAds: script slot is missing data-ad-src"); } const script = document.createElement("script"); script.src = data.src; script.async = true; // Success / failure are asynchronous for network-loaded scripts. script.onload = () => { setState(slot, "loaded"); if (typeof options.onLoad === "function") options.onLoad(slot, data); }; script.onerror = () => { setState(slot, "failed"); if (typeof options.onError === "function") { options.onError(slot, new Error(`lazyAds: failed to load ${data.src}`)); } }; slot.appendChild(script);}/** * Loads an "iframe" format ad by injecting an <iframe> into the slot. * @param {Element} slot * @param {Object} data * @param {LazyAdsOptions} options */function loadIframe(slot, data, options) { if (!data.src) { throw new Error("lazyAds: iframe slot is missing data-ad-src"); } const iframe = document.createElement("iframe"); iframe.src = data.src; if (data.width) iframe.width = data.width; if (data.height) iframe.height = data.height; iframe.loading = "lazy"; iframe.setAttribute("frameborder", "0"); iframe.setAttribute("scrolling", "no"); // Restrict what the ad iframe can do; loosen only if your provider needs it. iframe.setAttribute("sandbox", "allow-scripts allow-same-origin allow-popups"); iframe.onload = () => { setState(slot, "loaded"); if (typeof options.onLoad === "function") options.onLoad(slot, data); }; iframe.onerror = () => { setState(slot, "failed"); if (typeof options.onError === "function") { options.onError(slot, new Error(`lazyAds: failed to load ${data.src}`)); } }; slot.appendChild(iframe);}/** * Hands the slot off to a caller-provided render callback (third-party SDKs). * @param {Element} slot * @param {Object} data * @param {LazyAdsOptions} options */function loadCallback(slot, data, options) { if (typeof options.render !== "function") { throw new Error( 'lazyAds: slot uses data-ad-format="callback" but no options.render was provided', ); } // The render callback is responsible for actually drawing the ad. If it // throws, the surrounding try/catch in loadSlot() marks the slot failed. options.render(slot, data); setState(slot, "loaded"); if (typeof options.onLoad === "function") options.onLoad(slot, data);}/** * Loads a single ad slot according to its format, with robust error handling. * @param {Element} slot * @param {LazyAdsOptions} options */function loadSlot(slot, options) { // Guard against double-loading (e.g. if observer fires twice before unobserve). if (slot.getAttribute("data-ad-state")) return; setState(slot, "loading"); const data = readSlotData(slot); try { switch (data.format) { case "script": loadScript(slot, data, options); break; case "iframe": loadIframe(slot, data, options); break; case "callback": loadCallback(slot, data, options); break; default: throw new Error(`lazyAds: unknown ad format "${data.format}"`); } } catch (error) { // Synchronous failures (bad config, throwing render callback) land here. setState(slot, "failed"); if (typeof options.onError === "function") { options.onError(slot, error instanceof Error ? error : new Error(String(error))); } else { // Never let a single broken slot bubble up and break page scripts. // eslint-disable-next-line no-console console.error(error); } }}/** * Initialises lazy ad loading. * * @param {LazyAdsOptions} [userOptions] * @returns {{ observer: IntersectionObserver | null, destroy: () => void, refresh: () => void }} * Control object. `refresh()` re-scans the DOM for new slots (useful * for SPAs / infinite scroll); `destroy()` disconnects the observer. */export function initLazyAds(userOptions = {}) { const options = { ...DEFAULT_OPTIONS, ...userOptions }; // Fallback: if IntersectionObserver is unavailable, load everything eagerly. if (typeof IntersectionObserver === "undefined") { const slots = document.querySelectorAll(options.selector); slots.forEach((slot) => loadSlot(slot, options)); return { observer: null, destroy() {}, refresh() {} }; } const observer = new IntersectionObserver( (entries, obs) => { for (const entry of entries) { if (!entry.isIntersecting) continue; const slot = entry.target; // Stop observing before loading so the callback can't fire twice. obs.unobserve(slot); loadSlot(slot, options); } }, { root: options.root, rootMargin: options.rootMargin, threshold: options.threshold, }, ); /** Scans the DOM and observes any not-yet-tracked slots. */ function refresh() { const slots = document.querySelectorAll(options.selector); slots.forEach((slot) => { // Skip slots already loaded or already being observed. if (slot.getAttribute("data-ad-state")) return; if (slot.getAttribute("data-ad-observed") === "true") return; slot.setAttribute("data-ad-observed", "true"); observer.observe(slot); }); } refresh(); return { observer, refresh, destroy() { observer.disconnect(); }, };}export default initLazyAds;/** * preconnect.js — Cap. 17: "Aparecer ou Sumir" * * Injects <link rel="preconnect"> (with a <link rel="dns-prefetch"> fallback) * for a list of ad / CDN origins. Preconnecting warms up the DNS + TCP + TLS * handshake before the ad script is actually requested, shaving latency off * the eventual load without downloading anything up front. * * We keep it dependency-free and idempotent: calling it twice with the same * origins will not create duplicate <link> tags. *//** * Normalises a URL/origin string down to its origin (scheme + host + port). * Returns null for invalid input so bad entries are skipped rather than thrown. * @param {string} input * @returns {string | null} */function toOrigin(input) { try { // Accept full URLs ("https://ads.example.com/x") or bare origins. const url = new URL(input, window.location.href); return url.origin; } catch { return null; }}/** * True if a <link> with the given rel+href already exists in <head>. * @param {string} rel * @param {string} href */function linkExists(rel, href) { return Boolean( document.head.querySelector(`link[rel="${rel}"][href="${href}"]`), );}/** * Creates and appends a <link> to <head>. * @param {string} rel * @param {string} href * @param {boolean} crossOrigin */function appendLink(rel, href, crossOrigin) { if (linkExists(rel, href)) return; // de-dupe const link = document.createElement("link"); link.rel = rel; link.href = href; // preconnect to CORS resources (most ad tags) needs the crossorigin attr, // otherwise the browser opens a *second* connection for the real request. if (crossOrigin) link.crossOrigin = "anonymous"; document.head.appendChild(link);}/** * Adds preconnect (+ dns-prefetch fallback) hints for a list of origins. * * @param {string[] | string} origins One origin/URL or a list of them. * @param {{ crossOrigin?: boolean }} [opts] * crossOrigin defaults to true — most ad networks are cross-origin. * @returns {string[]} The de-duplicated list of origins that were added. * * @example * addPreconnect([ * "https://securepubads.g.doubleclick.net", * "https://www.googletagservices.com", * ]); */export function addPreconnect(origins, { crossOrigin = true } = {}) { if (typeof document === "undefined") return []; // SSR guard const list = Array.isArray(origins) ? origins : [origins]; // Normalise + de-duplicate origins before touching the DOM. const seen = new Set(); const added = []; for (const entry of list) { const origin = toOrigin(entry); if (!origin) continue; // skip invalid if (seen.has(origin)) continue; // skip duplicates within this call seen.add(origin); // dns-prefetch is the fallback for browsers that ignore preconnect and is // cheap, so we add both. preconnect does the heavy lifting where supported. appendLink("dns-prefetch", origin, false); appendLink("preconnect", origin, crossOrigin); added.push(origin); } return added;}export default addPreconnect;Dúvidas
Perguntas frequentes
Por que carregar anúncios sob demanda?
Scripts de anúncio são pesados e bloqueiam a renderização. Adiar o carregamento até o slot chegar perto da viewport melhora LCP, INP e a experiência — sem perder impressões visíveis.
E se o IntersectionObserver não existir?
O módulo tem fallback: se a API não estiver disponível, ele carrega todos os slots imediatamente, de forma segura.
Suporta quais formatos?
Três: script (injeta <script>), iframe (injeta <iframe>) e callback (entrega o slot para um SDK terceiro). Todos com tratamento de erro por slot.
Implementação
Prefere que eu implemente?
Conte o seu caso numa conversa e a gente define o escopo juntos.
Agendar conversa