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 #1aguardando viewport…
Slot de anúncio #2aguardando viewport…
Slot de anúncio #3aguardando viewport…
Slot de anúncio #4aguardando viewport…
Slot de anúncio #5aguardando viewport…
Slot de anúncio #6aguardando 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 the
5 * heaviest things on a page and hurt Core Web Vitals; loading them only when
6 * they are about to enter the viewport keeps the initial render fast while
7 * 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 to
24 * a third-party SDK (e.g. googletag.display, Prebid, etc.).
25 *
26 * Everything is wrapped in try/catch and per-element error handling so one
27 * broken slot never breaks the others.
28 */ /**
29 * @typedef {Object} LazyAdsOptions
30 * @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: undefined
46};
47/**
48 * Reads the `data-ad-*` attributes off a slot element into a plain object.
49 * @param {Element} slot
50 */ 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: ds
60 };
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} slot
66 * @param {"loading"|"loaded"|"failed"} state
67 */ 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} slot
74 * @param {Object} data
75 * @param {LazyAdsOptions} options
76 */ 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} slot
99 * @param {Object} data
100 * @param {LazyAdsOptions} options
101 */ 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} slot
129 * @param {Object} data
130 * @param {LazyAdsOptions} options
131 */ 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 it
136 // 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} slot
144 * @param {LazyAdsOptions} options
145 */ 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-console
172 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 (useful
182 * for SPAs / infinite scroll); `destroy()` disconnects the observer.
183 */ export function initLazyAds(userOptions = {}) {
184 const options = {
185 ...DEFAULT_OPTIONS,
186 ...userOptions
187 };
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.threshold
210 });
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 + TLS
6 * handshake before the ad script is actually requested, shaving latency off
7 * the eventual load without downloading anything up front.
8 *
9 * We keep it dependency-free and idempotent: calling it twice with the same
10 * 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} input
15 * @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} rel
28 * @param {string} href
29 */ 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} rel
35 * @param {string} href
36 * @param {boolean} crossOrigin
37 */ function appendLink(rel, href, crossOrigin) {
38 if (linkExists(rel, href)) return; // de-dupe
39 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 * @example
56 * 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 guard
62 const list = Array.isArray(origins) ? origins : [
63 origins
64 ];
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 invalid
71 if (seen.has(origin)) continue; // skip duplicates within this call
72 seen.add(origin);
73 // dns-prefetch is the fallback for browsers that ignore preconnect and is
74 // 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.