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.
Slots carregados: 0/6
Demonstração ao vivo
Role para baixo ↓
Slot de anúncio #1 — aguardando viewport…
Slot de anúncio #2 — aguardando viewport…
Slot de anúncio #3 — aguardando viewport…
Slot de anúncio #4 — aguardando viewport…
Slot de anúncio #5 — aguardando viewport…
Slot de anúncio #6 — aguardando viewport…
Código do módulo
IntersectionObserver com tratamento de erro + helper de preconnect. Copie ou baixe o .zip.
lazyAds.js
1/**2 * lazyAds.js — Cap. 17: "Aparecer ou Sumir"3 *4 * Lazy loader for ad slots based on IntersectionObserver. Ads are one of the5 * heaviest things on a page and hurt Core Web Vitals; loading them only when6 * they are about to enter the viewport keeps the initial render fast while7 * still monetising below-the-fold inventory.8 *9 * Markup contract — mark each ad container with `data-ad-slot`:10 *11 * <div data-ad-slot data-ad-format="script"12 * data-ad-src="https://ads.example.com/tag.js"></div>13 *14 * <div data-ad-slot data-ad-format="iframe"15 * data-ad-src="https://ads.example.com/unit.html"16 * data-ad-width="300" data-ad-height="250"></div>17 *18 * <div data-ad-slot data-ad-format="callback" data-ad-id="sidebar-1"></div>19 *20 * Supported formats:21 * - "script" : injects a <script src="data-ad-src"> into the slot.22 * - "iframe" : injects an <iframe src="data-ad-src"> into the slot.23 * - "callback" : calls options.render(slot, slotData) so you can hand off to24 * a third-party SDK (e.g. googletag.display, Prebid, etc.).25 *26 * Everything is wrapped in try/catch and per-element error handling so one27 * broken slot never breaks the others.28 */ /**29 * @typedef {Object} LazyAdsOptions30 * @property {string} [selector] CSS selector for ad slots. Default "[data-ad-slot]".31 * @property {string} [rootMargin] IntersectionObserver rootMargin. Default "200px".32 * @property {number} [threshold] IntersectionObserver threshold. Default 0.33 * @property {Element} [root] Optional scroll root. Default null (viewport).34 * @property {(slot: Element, data: Object) => void} [render]35 * Callback used for slots with data-ad-format="callback".36 * @property {(slot: Element, data: Object) => void} [onLoad] Called after a slot loads.37 * @property {(slot: Element, error: Error) => void} [onError] Called when a slot fails.38 */ /** Sensible defaults, merged with whatever the caller passes in. */ const DEFAULT_OPTIONS = {39 selector: "[data-ad-slot]",40 rootMargin: "200px",41 threshold: 0,42 root: null,43 render: undefined,44 onLoad: undefined,45 onError: undefined46};47/**48 * Reads the `data-ad-*` attributes off a slot element into a plain object.49 * @param {Element} slot50 */ function readSlotData(slot) {51 const ds = slot.dataset || {};52 return {53 format: ds.adFormat || "script",54 src: ds.adSrc || "",55 id: ds.adId || "",56 width: ds.adWidth || "",57 height: ds.adHeight || "",58 // Everything else in the dataset is passed through for callbacks.59 dataset: ds60 };61}62/**63 * Marks a slot's loading state via a data attribute and CSS-friendly class,64 * so you can style loading / loaded / failed states.65 * @param {Element} slot66 * @param {"loading"|"loaded"|"failed"} state67 */ function setState(slot, state) {68 slot.setAttribute("data-ad-state", state);69}70/**71 * Loads a "script" format ad by injecting a <script> tag into the slot.72 * Resolves/rejects via the provided callbacks (onerror handling included).73 * @param {Element} slot74 * @param {Object} data75 * @param {LazyAdsOptions} options76 */ function loadScript(slot, data, options) {77 if (!data.src) {78 throw new Error("lazyAds: script slot is missing data-ad-src");79 }80 const script = document.createElement("script");81 script.src = data.src;82 script.async = true;83 // Success / failure are asynchronous for network-loaded scripts.84 script.onload = ()=>{85 setState(slot, "loaded");86 if (typeof options.onLoad === "function") options.onLoad(slot, data);87 };88 script.onerror = ()=>{89 setState(slot, "failed");90 if (typeof options.onError === "function") {91 options.onError(slot, new Error(`lazyAds: failed to load ${data.src}`));92 }93 };94 slot.appendChild(script);95}96/**97 * Loads an "iframe" format ad by injecting an <iframe> into the slot.98 * @param {Element} slot99 * @param {Object} data100 * @param {LazyAdsOptions} options101 */ function loadIframe(slot, data, options) {102 if (!data.src) {103 throw new Error("lazyAds: iframe slot is missing data-ad-src");104 }105 const iframe = document.createElement("iframe");106 iframe.src = data.src;107 if (data.width) iframe.width = data.width;108 if (data.height) iframe.height = data.height;109 iframe.loading = "lazy";110 iframe.setAttribute("frameborder", "0");111 iframe.setAttribute("scrolling", "no");112 // Restrict what the ad iframe can do; loosen only if your provider needs it.113 iframe.setAttribute("sandbox", "allow-scripts allow-same-origin allow-popups");114 iframe.onload = ()=>{115 setState(slot, "loaded");116 if (typeof options.onLoad === "function") options.onLoad(slot, data);117 };118 iframe.onerror = ()=>{119 setState(slot, "failed");120 if (typeof options.onError === "function") {121 options.onError(slot, new Error(`lazyAds: failed to load ${data.src}`));122 }123 };124 slot.appendChild(iframe);125}126/**127 * Hands the slot off to a caller-provided render callback (third-party SDKs).128 * @param {Element} slot129 * @param {Object} data130 * @param {LazyAdsOptions} options131 */ function loadCallback(slot, data, options) {132 if (typeof options.render !== "function") {133 throw new Error('lazyAds: slot uses data-ad-format="callback" but no options.render was provided');134 }135 // The render callback is responsible for actually drawing the ad. If it136 // throws, the surrounding try/catch in loadSlot() marks the slot failed.137 options.render(slot, data);138 setState(slot, "loaded");139 if (typeof options.onLoad === "function") options.onLoad(slot, data);140}141/**142 * Loads a single ad slot according to its format, with robust error handling.143 * @param {Element} slot144 * @param {LazyAdsOptions} options145 */ function loadSlot(slot, options) {146 // Guard against double-loading (e.g. if observer fires twice before unobserve).147 if (slot.getAttribute("data-ad-state")) return;148 setState(slot, "loading");149 const data = readSlotData(slot);150 try {151 switch(data.format){152 case "script":153 loadScript(slot, data, options);154 break;155 case "iframe":156 loadIframe(slot, data, options);157 break;158 case "callback":159 loadCallback(slot, data, options);160 break;161 default:162 throw new Error(`lazyAds: unknown ad format "${data.format}"`);163 }164 } catch (error) {165 // Synchronous failures (bad config, throwing render callback) land here.166 setState(slot, "failed");167 if (typeof options.onError === "function") {168 options.onError(slot, error instanceof Error ? error : new Error(String(error)));169 } else {170 // Never let a single broken slot bubble up and break page scripts.171 // eslint-disable-next-line no-console172 console.error(error);173 }174 }175}176/**177 * Initialises lazy ad loading.178 *179 * @param {LazyAdsOptions} [userOptions]180 * @returns {{ observer: IntersectionObserver | null, destroy: () => void, refresh: () => void }}181 * Control object. `refresh()` re-scans the DOM for new slots (useful182 * for SPAs / infinite scroll); `destroy()` disconnects the observer.183 */ export function initLazyAds(userOptions = {}) {184 const options = {185 ...DEFAULT_OPTIONS,186 ...userOptions187 };188 // Fallback: if IntersectionObserver is unavailable, load everything eagerly.189 if (typeof IntersectionObserver === "undefined") {190 const slots = document.querySelectorAll(options.selector);191 slots.forEach((slot)=>loadSlot(slot, options));192 return {193 observer: null,194 destroy () {},195 refresh () {}196 };197 }198 const observer = new IntersectionObserver((entries, obs)=>{199 for (const entry of entries){200 if (!entry.isIntersecting) continue;201 const slot = entry.target;202 // Stop observing before loading so the callback can't fire twice.203 obs.unobserve(slot);204 loadSlot(slot, options);205 }206 }, {207 root: options.root,208 rootMargin: options.rootMargin,209 threshold: options.threshold210 });211 /** Scans the DOM and observes any not-yet-tracked slots. */ function refresh() {212 const slots = document.querySelectorAll(options.selector);213 slots.forEach((slot)=>{214 // Skip slots already loaded or already being observed.215 if (slot.getAttribute("data-ad-state")) return;216 if (slot.getAttribute("data-ad-observed") === "true") return;217 slot.setAttribute("data-ad-observed", "true");218 observer.observe(slot);219 });220 }221 refresh();222 return {223 observer,224 refresh,225 destroy () {226 observer.disconnect();227 }228 };229}230export default initLazyAds;
preconnect.js
1/**2 * preconnect.js — Cap. 17: "Aparecer ou Sumir"3 *4 * Injects <link rel="preconnect"> (with a <link rel="dns-prefetch"> fallback)5 * for a list of ad / CDN origins. Preconnecting warms up the DNS + TCP + TLS6 * handshake before the ad script is actually requested, shaving latency off7 * the eventual load without downloading anything up front.8 *9 * We keep it dependency-free and idempotent: calling it twice with the same10 * origins will not create duplicate <link> tags.11 */ /**12 * Normalises a URL/origin string down to its origin (scheme + host + port).13 * Returns null for invalid input so bad entries are skipped rather than thrown.14 * @param {string} input15 * @returns {string | null}16 */ function toOrigin(input) {17 try {18 // Accept full URLs ("https://ads.example.com/x") or bare origins.19 const url = new URL(input, window.location.href);20 return url.origin;21 } catch {22 return null;23 }24}25/**26 * True if a <link> with the given rel+href already exists in <head>.27 * @param {string} rel28 * @param {string} href29 */ function linkExists(rel, href) {30 return Boolean(document.head.querySelector(`link[rel="${rel}"][href="${href}"]`));31}32/**33 * Creates and appends a <link> to <head>.34 * @param {string} rel35 * @param {string} href36 * @param {boolean} crossOrigin37 */ function appendLink(rel, href, crossOrigin) {38 if (linkExists(rel, href)) return; // de-dupe39 const link = document.createElement("link");40 link.rel = rel;41 link.href = href;42 // preconnect to CORS resources (most ad tags) needs the crossorigin attr,43 // otherwise the browser opens a *second* connection for the real request.44 if (crossOrigin) link.crossOrigin = "anonymous";45 document.head.appendChild(link);46}47/**48 * Adds preconnect (+ dns-prefetch fallback) hints for a list of origins.49 *50 * @param {string[] | string} origins One origin/URL or a list of them.51 * @param {{ crossOrigin?: boolean }} [opts]52 * crossOrigin defaults to true — most ad networks are cross-origin.53 * @returns {string[]} The de-duplicated list of origins that were added.54 *55 * @example56 * addPreconnect([57 * "https://securepubads.g.doubleclick.net",58 * "https://www.googletagservices.com",59 * ]);60 */ export function addPreconnect(origins, { crossOrigin = true } = {}) {61 if (typeof document === "undefined") return []; // SSR guard62 const list = Array.isArray(origins) ? origins : [63 origins64 ];65 // Normalise + de-duplicate origins before touching the DOM.66 const seen = new Set();67 const added = [];68 for (const entry of list){69 const origin = toOrigin(entry);70 if (!origin) continue; // skip invalid71 if (seen.has(origin)) continue; // skip duplicates within this call72 seen.add(origin);73 // dns-prefetch is the fallback for browsers that ignore preconnect and is74 // cheap, so we add both. preconnect does the heavy lifting where supported.75 appendLink("dns-prefetch", origin, false);76 appendLink("preconnect", origin, crossOrigin);77 added.push(origin);78 }79 return added;80}81export default addPreconnect;
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.