Pular para o conteúdo

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
/**
* 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
/**
* 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