- 1. Ad Tech Fundamentals
- 2. Click Tracking
- 3. Performance Optimization
- 4. Building Ad Components
- 5. SPA-Specific Ad Problems
- 6. Event Tracking at Scale
- 7. Fraud Detection & Invalid Traffic
- 8. Security Considerations
- 9. Bundle Optimization
- 10. Key Metrics
- 11. System Design Question
- 12. Interview-Winning Patterns
Definition: An impression is counted when an ad is successfully rendered or becomes viewable on the page.
Key Point: Not just loaded into the DOM, but actually visible to the user.
| Type | Definition | When Counted |
|---|---|---|
| Served Impression | Ad request was fulfilled and HTML/creative was delivered | When ad markup is inserted into DOM |
| Viewable Impression | Ad meets viewability criteria (IAB standards) | When 50%+ of ad pixels are visible for 1+ second |
IAB Viewability Standard:
- Display ads: ≥50% of pixels visible for ≥1 continuous second
- Video ads: ≥50% of pixels visible for ≥2 continuous seconds
Challenge: SPAs don't have traditional page loads, so you need to track impressions programmatically.
Solution: Use IntersectionObserver API
function trackImpression(adElement, adId) {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting && entry.intersectionRatio >= 0.5) {
// Start visibility timer
const timer = setTimeout(() => {
// Fire impression event
sendImpression(adId);
observer.disconnect();
}, 1000); // 1 second visibility threshold
// Store timer to clear if element becomes hidden
entry.target.dataset.impressionTimer = timer;
} else {
// Element no longer visible, clear timer
const timer = entry.target.dataset.impressionTimer;
if (timer) clearTimeout(timer);
}
});
},
{ threshold: 0.5 } // 50% visibility threshold
);
observer.observe(adElement);
return observer;
}Key Points for Interview:
- Use
threshold: 0.5for 50% visibility - Add time-based check (1 second) to meet IAB standards
- Clean up observer when impression is counted
- Handle cases where ad scrolls out of view before timer completes
❌ Bad Approach:
// Don't rely solely on 'click' event
adElement.addEventListener('click', trackClick);✅ Good Approach:
// Use mousedown/pointerdown for earlier capture
adElement.addEventListener('pointerdown', (e) => {
const clickData = {
adId: adElement.dataset.adId,
timestamp: Date.now(),
coordinates: { x: e.clientX, y: e.clientY },
pointerType: e.pointerType // 'mouse', 'pen', 'touch'
};
// Send immediately before navigation
navigator.sendBeacon('/track-click', JSON.stringify(clickData));
});Why pointerdown instead of click?
- Fires earlier in the event chain
- Navigation might happen before
clickfires - Works across mouse, touch, and pen inputs
- Less likely to be lost during page unload
Problem: User navigates away before tracking request completes.
Solution: Use navigator.sendBeacon()
function trackClick(adId, metadata) {
const payload = JSON.stringify({
adId,
timestamp: Date.now(),
...metadata
});
// sendBeacon queues request even if page unloads
const success = navigator.sendBeacon('/track-click', payload);
if (!success) {
// Fallback: queue in localStorage/IndexedDB
queueOfflineEvent('click', payload);
}
}Benefits of sendBeacon:
- Non-blocking (doesn't delay navigation)
- Browser guarantees delivery even after page unload
- POST request with configurable payload
- Returns boolean indicating if queued successfully
Expected Points:
- Async Loading
// Load ad SDK asynchronously
const script = document.createElement('script');
script.src = 'https://ads.example.com/sdk.js';
script.async = true;
script.defer = true;
document.head.appendChild(script);- Lazy Loading
// Only load ads when they're about to enter viewport
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
loadAd(entry.target);
observer.unobserve(entry.target);
}
});
}, { rootMargin: '200px' }); // Pre-load 200px before visible- Code Splitting
// Dynamic import for ad formats
async function loadVideoAd(container) {
const { VideoAdRenderer } = await import('./video-ad-renderer');
new VideoAdRenderer(container).render();
}- No Render Blocking
<!-- ❌ Bad: Blocks rendering -->
<script src="/ads.js"></script>
<!-- ✅ Good: Non-blocking -->
<script src="/ads.js" async></script>- Prevent Layout Shifts (CLS)
/* Reserve space before ad loads */
.ad-container {
width: 300px;
height: 250px;
background: #f0f0f0; /* Placeholder background */
}Why Microsoft Cares:
- CLS hurts SEO rankings (Core Web Vitals)
- Bad user experience → lower engagement
- Lower engagement = lower ad revenue
❌ Bad Example:
<!-- Ad loads and pushes content down -->
<div id="ad"></div>
<main>Content here...</main>✅ Good Example:
<!-- Pre-allocated space -->
<div id="ad" style="width: 300px; height: 250px;">
<!-- Ad loads into fixed space -->
</div>
<main>Content here...</main>CSS Approach:
.ad-slot {
width: 300px;
height: 250px;
min-height: 250px; /* Prevents collapse */
background: #f5f5f5;
position: relative;
}
/* Responsive ads */
.ad-slot-responsive {
aspect-ratio: 16 / 9;
width: 100%;
max-width: 728px;
}Interview Answer Template:
"CLS is critical for ads because layout shifts directly impact user experience and SEO. We prevent this by pre-allocating space with fixed dimensions before the ad loads. For responsive ads, we use aspect-ratio CSS or calculate dimensions based on viewport size. This ensures content doesn't jump when ads render, which improves both Core Web Vitals scores and user engagement—both tied to revenue."
Basic Implementation:
import { useEffect, useRef, useState } from 'react';
function AdSlot({ adId, width = 300, height = 250, onImpression }) {
const ref = useRef(null);
const [isLoaded, setIsLoaded] = useState(false);
const impressionFired = useRef(false);
useEffect(() => {
const element = ref.current;
if (!element) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting && !impressionFired.current) {
// Load ad creative
loadAd(adId, element).then(() => {
setIsLoaded(true);
});
// Start viewability timer
setTimeout(() => {
if (!impressionFired.current) {
impressionFired.current = true;
onImpression?.(adId);
}
}, 1000);
observer.disconnect();
}
},
{ threshold: 0.5 }
);
observer.observe(element);
return () => {
observer.disconnect();
};
}, [adId, onImpression]);
return (
<div
ref={ref}
className="ad-slot"
style={{ width, height }}
data-ad-id={adId}
>
{!isLoaded && <div className="ad-placeholder">Loading...</div>}
</div>
);
}Problem: Impressions fire multiple times due to re-renders or component remounts.
Solutions:
- Use Ref to Track State
const impressionFired = useRef(false);
useEffect(() => {
if (!impressionFired.current) {
fireImpression(adId);
impressionFired.current = true;
}
}, [adId]);- Unique Impression IDs
const impressionId = `${adId}-${Date.now()}-${Math.random()}`;
const fired = new Set(); // Global or context-based
if (!fired.has(impressionId)) {
fireImpression(adId, impressionId);
fired.add(impressionId);
}- Server-Side Deduplication
// Send client-generated UUID
const clientId = crypto.randomUUID();
sendImpression({ adId, clientId, timestamp: Date.now() });
// Server deduplicates based on clientId + adId + time windowProblem: In SPAs, route changes don't reload ads automatically.
Solution: Listen to route changes and refresh ads
import { useLocation } from 'react-router-dom';
function AdSlot({ adId }) {
const location = useLocation();
const ref = useRef(null);
const [key, setKey] = useState(0);
useEffect(() => {
// Refresh ad on route change
setKey(prev => prev + 1);
// Clean up previous ad
return () => {
cleanupAd(ref.current);
};
}, [location.pathname]);
return <div ref={ref} key={key} data-ad-id={adId} />;
}Advanced Pattern with Ad Manager:
// AdManager context
const AdManagerContext = createContext();
function AdManager({ children }) {
const location = useLocation();
const [adSlots, setAdSlots] = useState(new Map());
useEffect(() => {
// Refresh all ads on route change
adSlots.forEach((slot, id) => {
refreshAd(id, slot);
});
}, [location.pathname]);
const registerSlot = (id, element) => {
setAdSlots(prev => new Map(prev).set(id, element));
};
const unregisterSlot = (id) => {
setAdSlots(prev => {
const next = new Map(prev);
next.delete(id);
return next;
});
};
return (
<AdManagerContext.Provider value={{ registerSlot, unregisterSlot }}>
{children}
</AdManagerContext.Provider>
);
}Key Issues:
- Ads don't automatically reload
- Old ad references remain in memory
- Impression tracking breaks
- Event listeners persist
Solution Pattern:
function AdContainer() {
const location = useLocation();
const adRefs = useRef(new Map());
useEffect(() => {
// Clean up old ads
adRefs.current.forEach((ad) => {
ad.destroy(); // Remove listeners, clear timers
});
adRefs.current.clear();
// Request new ads for current page
refreshAds();
return () => {
// Cleanup on unmount
adRefs.current.forEach((ad) => ad.destroy());
};
}, [location.pathname]);
return <div className="ad-container">{/* Ad slots */}</div>;
}Common Sources of Leaks:
- IntersectionObserver not disconnected
- Event listeners not removed
- Timers not cleared
- Global state not cleaned
Comprehensive Cleanup Pattern:
function AdSlot({ adId }) {
const ref = useRef(null);
const observerRef = useRef(null);
const timersRef = useRef([]);
useEffect(() => {
const element = ref.current;
// Setup observer
observerRef.current = new IntersectionObserver(callback);
observerRef.current.observe(element);
// Setup timers
const timer1 = setTimeout(() => {}, 1000);
const timer2 = setInterval(() => {}, 5000);
timersRef.current = [timer1, timer2];
// Setup event listeners
const handleClick = () => {};
element.addEventListener('click', handleClick);
// Cleanup function
return () => {
// Disconnect observer
observerRef.current?.disconnect();
// Clear all timers
timersRef.current.forEach(clearTimeout);
timersRef.current.forEach(clearInterval);
// Remove event listeners
element?.removeEventListener('click', handleClick);
// Clear references
observerRef.current = null;
timersRef.current = [];
};
}, [adId]);
return <div ref={ref} />;
}Problem: Sending millions of individual events blocks the main thread and overloads servers.
Solution: Batch events and flush periodically
class EventTracker {
constructor() {
this.queue = [];
this.maxBatchSize = 20;
this.flushInterval = 5000; // 5 seconds
this.startAutoFlush();
}
track(event) {
this.queue.push({
...event,
timestamp: Date.now(),
clientId: this.getClientId()
});
// Flush if batch is full
if (this.queue.length >= this.maxBatchSize) {
this.flush();
}
}
async flush() {
if (this.queue.length === 0) return;
const batch = this.queue.splice(0, this.maxBatchSize);
try {
// Non-blocking send
if (navigator.sendBeacon) {
navigator.sendBeacon('/events', JSON.stringify(batch));
} else {
// Fallback
fetch('/events', {
method: 'POST',
body: JSON.stringify(batch),
keepalive: true
});
}
} catch (error) {
// Retry or queue for offline
this.handleFailure(batch);
}
}
startAutoFlush() {
setInterval(() => this.flush(), this.flushInterval);
// Flush on page unload
window.addEventListener('beforeunload', () => this.flush());
}
handleFailure(batch) {
// Store in IndexedDB for retry
this.storeOffline(batch);
}
}
// Usage
const tracker = new EventTracker();
tracker.track({ type: 'impression', adId: '123' });
tracker.track({ type: 'click', adId: '456' });Throttle/Debounce for High-Frequency Events:
// Throttle: Execute at most once per interval
function throttle(func, limit) {
let inThrottle;
return function(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
// Track scroll events (throttled)
const trackScroll = throttle((scrollY) => {
tracker.track({ type: 'scroll', position: scrollY });
}, 1000);
window.addEventListener('scroll', () => trackScroll(window.scrollY));Using IndexedDB for Reliability:
class OfflineEventQueue {
constructor() {
this.dbName = 'AdEventsDB';
this.storeName = 'events';
this.initDB();
}
async initDB() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, 1);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
this.db = request.result;
resolve(this.db);
};
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains(this.storeName)) {
db.createObjectStore(this.storeName, {
keyPath: 'id',
autoIncrement: true
});
}
};
});
}
async add(event) {
const transaction = this.db.transaction([this.storeName], 'readwrite');
const store = transaction.objectStore(this.storeName);
return store.add(event);
}
async getAll() {
const transaction = this.db.transaction([this.storeName], 'readonly');
const store = transaction.objectStore(this.storeName);
return new Promise((resolve) => {
const request = store.getAll();
request.onsuccess = () => resolve(request.result);
});
}
async clear() {
const transaction = this.db.transaction([this.storeName], 'readwrite');
const store = transaction.objectStore(this.storeName);
return store.clear();
}
async syncWhenOnline() {
if (!navigator.onLine) return;
const events = await this.getAll();
if (events.length === 0) return;
try {
await fetch('/events/bulk', {
method: 'POST',
body: JSON.stringify(events)
});
await this.clear();
} catch (error) {
console.error('Sync failed:', error);
}
}
}
// Auto-sync when back online
window.addEventListener('online', () => {
offlineQueue.syncWhenOnline();
});Web Worker for Background Processing:
// event-worker.js
self.addEventListener('message', (e) => {
const { type, data } = e.data;
if (type === 'track') {
// Process event in background
const processed = processEvent(data);
self.postMessage({ type: 'processed', data: processed });
}
});
// Main thread
const worker = new Worker('event-worker.js');
worker.postMessage({ type: 'track', data: eventData });Interview-Winning Talking Points:
- "We don't block the main thread"
- "Fire-and-forget telemetry with retry logic"
- "Batching reduces network overhead by 95%"
- "IndexedDB ensures zero data loss even when offline"
Key Signals to Track:
class FraudDetector {
detectSuspiciousClick(event, adElement) {
const signals = {
// 1. Time-to-click (too fast = bot)
timeToClick: Date.now() - adElement.dataset.renderTime,
// 2. Click coordinates
coordinates: { x: event.clientX, y: event.clientY },
// 3. Repeated clicks at same position
isSamePosition: this.checkDuplicatePosition(event),
// 4. User agent
userAgent: navigator.userAgent,
// 5. Ad visibility
isVisible: this.checkVisibility(adElement),
// 6. Tab visibility
isTabActive: !document.hidden,
// 7. Pointer movement
hasPointerMovement: this.hasRecentPointerActivity(),
// 8. Touch vs mouse
inputType: event.pointerType || 'unknown'
};
return this.calculateFraudScore(signals);
}
checkDuplicatePosition(event) {
const key = `${event.clientX},${event.clientY}`;
const lastClick = this.clickHistory.get(key);
if (lastClick && Date.now() - lastClick < 1000) {
return true; // Same position within 1 second
}
this.clickHistory.set(key, Date.now());
return false;
}
checkVisibility(element) {
const rect = element.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= window.innerHeight &&
rect.right <= window.innerWidth &&
rect.width > 0 &&
rect.height > 0
);
}
hasRecentPointerActivity() {
// Track if user has moved mouse recently
return Date.now() - this.lastPointerMove < 5000;
}
calculateFraudScore(signals) {
let score = 0;
// Too fast click (< 100ms)
if (signals.timeToClick < 100) score += 30;
// Duplicate position
if (signals.isSamePosition) score += 25;
// Hidden ad
if (!signals.isVisible) score += 40;
// Background tab
if (!signals.isTabActive) score += 20;
// No pointer movement
if (!signals.hasPointerMovement) score += 15;
return {
score, // 0-100
signals,
isSuspicious: score > 50
};
}
}1. Visibility API Usage:
function trackAdVisibility(adId) {
// Detect if page is in background
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
// Ad not viewable in background tab
markAsNonViewable(adId);
}
});
}2. Pointer Movement Tracking:
let lastPointerMove = 0;
let pointerPath = [];
document.addEventListener('pointermove', (e) => {
lastPointerMove = Date.now();
pointerPath.push({ x: e.clientX, y: e.clientY, t: Date.now() });
// Keep only last 10 points
if (pointerPath.length > 10) pointerPath.shift();
});
function hasNaturalPointerMovement() {
if (pointerPath.length < 3) return false;
// Check for natural human movement (not perfectly straight line)
const variance = calculatePathVariance(pointerPath);
return variance > THRESHOLD;
}3. Click Pattern Analysis:
class ClickPatternAnalyzer {
constructor() {
this.clicks = [];
}
recordClick(event) {
this.clicks.push({
x: event.clientX,
y: event.clientY,
timestamp: Date.now(),
target: event.target.dataset.adId
});
// Check for suspicious patterns
if (this.detectRapidClicking()) {
this.flagAsSuspicious();
}
}
detectRapidClicking() {
const recentClicks = this.clicks.filter(
c => Date.now() - c.timestamp < 5000
);
// More than 5 clicks in 5 seconds = suspicious
return recentClicks.length > 5;
}
detectClickFarm() {
// Same coordinates, different ads
const positions = new Map();
this.clicks.forEach(click => {
const key = `${click.x},${click.y}`;
if (!positions.has(key)) {
positions.set(key, []);
}
positions.get(key).push(click.target);
});
// Multiple ads clicked at exact same position
for (const [pos, ads] of positions) {
if (new Set(ads).size > 3) return true;
}
return false;
}
}Interview Answer Template:
"For fraud detection on the frontend, I'd track multiple signals: time-to-click to catch bots that click instantly, coordinate tracking to detect click farms hitting the same pixel, visibility checks using IntersectionObserver to ensure ads are actually viewable, tab visibility via the Page Visibility API, and pointer movement heuristics to distinguish human behavior from automated scripts. These signals get combined into a fraud score that's sent to the backend for final validation."
1. XSS (Cross-Site Scripting)
Third-party ad creatives can inject malicious scripts:
<!-- Malicious ad creative -->
<div>
<img src="x" onerror="alert('XSS')" />
<script>stealCookies()</script>
</div>2. Clickjacking
Transparent iframes overlay on ads to steal clicks:
<!-- Attacker's page -->
<iframe
src="https://example.com/ad"
style="opacity: 0; position: absolute; top: 0;"
></iframe>
<button>Click me!</button> <!-- Actually clicks ad -->3. Malicious Creatives
Ad content that:
- Redirects to phishing sites
- Auto-downloads malware
- Crypto-mining scripts
- Data exfiltration
4. Ad Injection
Malware injects unwanted ads into legitimate sites.
1. Sandbox Iframes
<!-- Isolate ad content -->
<iframe
sandbox="allow-scripts allow-same-origin"
src="/ad-creative.html"
style="width: 300px; height: 250px;"
></iframe>Sandbox Attributes:
allow-scripts: Allow JavaScript (needed for ads)allow-same-origin: Allow same-origin access (careful!)allow-top-navigation: Allow redirects (often needed)- Omit
allow-formsif forms aren't needed
2. Content Security Policy (CSP)
<meta
http-equiv="Content-Security-Policy"
content="
default-src 'self';
script-src 'self' https://trusted-ad-cdn.com;
img-src 'self' https: data:;
frame-src https://trusted-ad-server.com;
connect-src 'self' https://analytics.example.com;
"
>3. Subresource Integrity (SRI)
<!-- Verify third-party scripts haven't been tampered with -->
<script
src="https://ads-cdn.com/sdk.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
crossorigin="anonymous"
></script>4. Input Sanitization
// Sanitize ad creative before rendering
function sanitizeAdCreative(html) {
const temp = document.createElement('div');
temp.textContent = html; // Converts to text, strips tags
return temp.innerHTML;
}
// Or use DOMPurify
import DOMPurify from 'dompurify';
const clean = DOMPurify.sanitize(dirtyHTML);5. Feature Policy / Permissions Policy
<!-- Restrict what ads can do -->
<iframe
src="/ad.html"
allow="
geolocation 'none';
microphone 'none';
camera 'none';
payment 'none';
"
></iframe>6. Monitoring & Validation
// Monitor for suspicious behavior
class AdSecurityMonitor {
monitorAdBehavior(iframe) {
// Detect redirect attempts
iframe.addEventListener('load', () => {
if (iframe.contentWindow.location !== originalUrl) {
this.flagSuspiciousAd(iframe);
}
});
// Monitor resource loading
this.observeResources(iframe);
// Check for crypto-mining (high CPU usage)
this.monitorCPUUsage();
}
observeResources(iframe) {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (this.isSuspiciousDomain(entry.name)) {
this.blockResource(entry.name);
this.reportViolation(iframe, 'suspicious-resource');
}
}
});
observer.observe({ entryTypes: ['resource'] });
}
monitorCPUUsage() {
// Detect crypto-mining or excessive CPU usage
setInterval(() => {
if (this.getCPUUsage() > 80) {
this.flagHighCPUAd();
}
}, 5000);
}
}Interview Answer Template:
"Ads are a major attack surface because we're loading third-party content. We mitigate this through multiple layers: sandboxed iframes to isolate ad content, CSP headers to restrict what scripts can run, SRI to verify third-party SDK integrity, and runtime monitoring to detect malicious behavior like unauthorized redirects or crypto-mining. We also maintain an allowlist of trusted ad servers and validate all creatives before serving."
Why This Matters for Ads:
- Ads SDK runs on every page
- Must be tiny (~10-20KB gzipped max)
- Users shouldn't wait for ads to see content
1. Separate Ads Bundle
// webpack.config.js
module.exports = {
entry: {
main: './src/index.js',
ads: './src/ads/index.js' // Separate bundle
},
output: {
filename: '[name].[contenthash].js',
path: path.resolve(__dirname, 'dist')
},
optimization: {
splitChunks: {
cacheGroups: {
ads: {
test: /[\\/]src[\\/]ads[\\/]/,
name: 'ads',
chunks: 'all'
}
}
}
}
};2. Dynamic Import by Ad Type
// Load only what's needed
async function renderAd(adConfig) {
const { type } = adConfig;
switch (type) {
case 'video':
const { VideoAdRenderer } = await import(
/* webpackChunkName: "video-ads" */
'./renderers/video'
);
return new VideoAdRenderer(adConfig);
case 'carousel':
const { CarouselAdRenderer } = await import(
/* webpackChunkName: "carousel-ads" */
'./renderers/carousel'
);
return new CarouselAdRenderer(adConfig);
case 'native':
const { NativeAdRenderer } = await import(
/* webpackChunkName: "native-ads" */
'./renderers/native'
);
return new NativeAdRenderer(adConfig);
default:
// Display ads are inline (most common)
return new DisplayAdRenderer(adConfig);
}
}3. Tree Shaking
// ads-sdk/index.js - Export only what's needed
export { trackImpression } from './tracking/impression';
export { trackClick } from './tracking/click';
// Don't export everything: export * from './utils'; ❌
// Consumer code
import { trackImpression } from 'ads-sdk';
// Only impression tracking code is bundled4. Lazy Load Ad SDK
// Core app loads first
window.addEventListener('load', () => {
// Load ads after page is interactive
requestIdleCallback(() => {
import('./ads-sdk').then(({ initAds }) => {
initAds();
});
});
});5. Format-Specific Loading
class AdManager {
async loadAdFormat(format) {
// Cache loaded formats
if (this.loadedFormats.has(format)) {
return this.loadedFormats.get(format);
}
const module = await this.importFormat(format);
this.loadedFormats.set(format, module);
return module;
}
async importFormat(format) {
const formatMap = {
display: () => import('./formats/display'),
video: () => import('./formats/video'),
native: () => import('./formats/native'),
interstitial: () => import('./formats/interstitial')
};
return formatMap[format]?.() || formatMap.display();
}
}1. Aggressive Caching
// Service Worker for ad SDK
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open('ads-sdk-v1').then((cache) => {
return cache.addAll([
'/ads-sdk.js',
'/ads-core.js'
]);
})
);
});
// Cache first, network fallback
self.addEventListener('fetch', (event) => {
if (event.request.url.includes('/ads-sdk')) {
event.respondWith(
caches.match(event.request).then((response) => {
return response || fetch(event.request);
})
);
}
});2. Versioned Assets
<!-- Content hash in filename for cache busting -->
<script src="/ads-sdk.a3f2b1c.js"></script>3. CDN + Cache Headers
// Server configuration
app.get('/ads-sdk.js', (req, res) => {
res.set({
'Cache-Control': 'public, max-age=31536000, immutable',
'Content-Type': 'application/javascript'
});
res.sendFile('ads-sdk.js');
});4. Preload Critical Resources
<!-- Preload ads SDK during idle time -->
<link rel="preload" href="/ads-sdk.js" as="script">
<link rel="dns-prefetch" href="https://ads-cdn.example.com">
<link rel="preconnect" href="https://ads-cdn.example.com">Bundle Size Optimization Checklist:
- ✅ Separate bundle for ads code
- ✅ Dynamic imports for ad formats
- ✅ Tree shaking enabled
- ✅ Minification + compression (gzip/brotli)
- ✅ Remove unused dependencies
- ✅ Use CDN for third-party libraries
- ✅ Code split by route and feature
- ✅ Lazy load non-critical code
Interview Answer Template:
"For ads bundle optimization, I'd create a separate bundle for ads code that's loaded asynchronously after the main app. We'd use dynamic imports to load only the ad format needed—for example, video ads only load when a video slot appears. Tree shaking removes unused code, and we'd serve everything through a CDN with aggressive caching (1 year max-age with content hashing). The goal is to keep the initial ads SDK under 15KB gzipped so it never impacts page load performance."
1. CTR (Click-Through Rate)
CTR = (Clicks / Impressions) × 100%
- Good CTR: 0.5% - 2% for display ads
- Indicates: Ad relevance and user engagement
- Impact: Higher CTR = higher revenue
2. CPM (Cost Per Mille)
CPM = (Cost / Impressions) × 1000
- Typical Range: $1 - $10 for display ads
- Indicates: Revenue per 1000 impressions
- Microsoft's Focus: Premium CPMs through quality placements
3. Fill Rate
Fill Rate = (Filled Ad Requests / Total Ad Requests) × 100%
- Target: >90%
- Indicates: How often ads are available to show
- Impact: Low fill rate = lost revenue
4. Viewability
Viewability = (Viewable Impressions / Measured Impressions) × 100%
- Industry Standard: >70%
- IAB Definition: 50% of pixels visible for 1+ second
- Impact: Higher viewability = higher CPMs
5. eCPM (Effective CPM)
eCPM = (Total Revenue / Total Impressions) × 1000
- Indicates: Actual revenue per 1000 impressions
- Used for: Comparing different ad formats/placements
1. CLS (Cumulative Layout Shift)
CLS = Σ(Impact Fraction × Distance Fraction)
- Target: <0.1 (Good), <0.25 (Needs Improvement)
- Why Critical: Core Web Vital, affects SEO
- Ad Impact: Ads are #1 cause of high CLS
2. TTI (Time to Interactive)
Time when page becomes fully interactive
- Target: <3.8s on mobile
- Ad Impact: Heavy ad scripts delay TTI
- Optimization: Async loading, code splitting
3. FID / INP (First Input Delay / Interaction to Next Paint)
FID: Time between first user interaction and browser response
INP: Responsiveness metric (replacing FID in 2024)
- Target: FID <100ms, INP <200ms
- Ad Impact: Blocking scripts increase FID/INP
- Fix: Non-blocking ad loading
4. LCP (Largest Contentful Paint)
Time when largest element becomes visible
- Target: <2.5s
- Ad Impact: Large ads can be LCP element
- Fix: Optimize ad image loading
Tracking Implementation:
// Track business metrics
class AdMetrics {
constructor() {
this.impressions = 0;
this.clicks = 0;
this.viewableImpressions = 0;
}
trackImpression(adId, isViewable) {
this.impressions++;
if (isViewable) this.viewableImpressions++;
// Send to analytics
this.send({
type: 'impression',
adId,
viewable: isViewable,
timestamp: Date.now()
});
}
trackClick(adId) {
this.clicks++;
this.send({
type: 'click',
adId,
timestamp: Date.now()
});
}
getCTR() {
return (this.clicks / this.impressions) * 100;
}
getViewability() {
return (this.viewableImpressions / this.impressions) * 100;
}
}
// Track performance metrics
class PerformanceMetrics {
trackCLS() {
let clsValue = 0;
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
clsValue += entry.value;
}
}
}).observe({ type: 'layout-shift', buffered: true });
return clsValue;
}
trackINP() {
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
const duration = entry.processingEnd - entry.processingStart;
this.send({
metric: 'INP',
value: duration,
entryType: entry.name
});
}
}).observe({ type: 'event', buffered: true });
}
}Interview Talking Points:
- "CLS is critical because layout shifts hurt both user experience and SEO, directly impacting revenue"
- "We optimize for viewability because viewable impressions command 2-3x higher CPMs"
- "TTI matters because slow pages increase bounce rate, reducing total impressions"
- "I monitor INP to ensure ads don't block user interactions"
Problem Statement:
"Design a client-side ad rendering system that handles multiple ad formats, tracks impressions and clicks, optimizes performance, and scales to millions of page views."
High-Level Architecture:
┌─────────────────────────────────────────────────────────┐
│ Page Application │
├─────────────────────────────────────────────────────────┤
│ │
│ ┌────────────┐ ┌─────────────┐ ┌──────────────┐ │
│ │ AdSlot 1 │ │ AdSlot 2 │ │ AdSlot 3 │ │
│ └─────┬──────┘ └──────┬──────┘ └──────┬───────┘ │
│ │ │ │ │
│ └────────────────┴─────────────────┘ │
│ │ │
│ ┌────────▼────────┐ │
│ │ Ad Manager │ │
│ └────────┬────────┘ │
│ │ │
│ ┌────────────────┼────────────────┐ │
│ │ │ │ │
│ ┌─────▼──────┐ ┌─────▼──────┐ ┌─────▼──────┐ │
│ │ Visibility │ │ Renderer │ │ Tracker │ │
│ │ Manager │ │ Manager │ │ Manager │ │
│ └────────────┘ └────────────┘ └────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────┐
│ Ad Server / Backend │
└─────────────────────────┘
Component Design:
1. AdSlot Component
import React, { useRef, useEffect, useState } from 'react';
import { useAdManager } from './AdManager';
function AdSlot({
id,
width = 300,
height = 250,
format = 'display',
targeting = {},
onLoad,
onImpression,
onClick
}) {
const containerRef = useRef(null);
const { registerSlot, unregisterSlot } = useAdManager();
const [isLoaded, setIsLoaded] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
const slotConfig = {
id,
element: containerRef.current,
width,
height,
format,
targeting,
callbacks: {
onLoad: () => {
setIsLoaded(true);
onLoad?.();
},
onImpression,
onClick,
onError: (err) => setError(err)
}
};
registerSlot(slotConfig);
return () => {
unregisterSlot(id);
};
}, [id, format, width, height]);
return (
<div
ref={containerRef}
className="ad-slot"
data-ad-id={id}
style={{
width: `${width}px`,
height: `${height}px`,
minHeight: `${height}px`, // Prevent CLS
position: 'relative',
backgroundColor: '#f5f5f5'
}}
>
{!isLoaded && !error && (
<div className="ad-placeholder">Loading ad...</div>
)}
{error && (
<div className="ad-error">Ad failed to load</div>
)}
</div>
);
}
export default AdSlot;2. Ad Manager
class AdManager {
constructor() {
this.slots = new Map();
this.visibilityManager = new VisibilityManager();
this.rendererManager = new RendererManager();
this.trackerManager = new TrackerManager();
this.config = {
lazyLoadMargin: '200px',
viewabilityThreshold: 0.5,
viewabilityDuration: 1000
};
}
registerSlot(slotConfig) {
const { id, element, callbacks } = slotConfig;
// Store slot reference
this.slots.set(id, { ...slotConfig, state: 'registered' });
// Setup visibility tracking
this.visibilityManager.observe(element, {
onVisible: () => this.loadAd(id),
onViewable: () => this.fireImpression(id),
threshold: this.config.viewabilityThreshold,
duration: this.config.viewabilityDuration
});
// Setup click tracking
element.addEventListener('pointerdown', (e) => {
this.trackerManager.trackClick(id, e);
callbacks.onClick?.(e);
});
}
async loadAd(id) {
const slot = this.slots.get(id);
if (!slot || slot.state !== 'registered') return;
// Update state
slot.state = 'loading';
try {
// Request ad from server
const adData = await this.requestAd(slot);
// Render ad
await this.rendererManager.render(slot, adData);
// Update state
slot.state = 'loaded';
slot.callbacks.onLoad?.();
} catch (error) {
slot.state = 'error';
slot.callbacks.onError?.(error);
this.trackerManager.trackError(id, error);
}
}
async requestAd(slot) {
const response = await fetch('/api/ads/request', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
slotId: slot.id,
format: slot.format,
dimensions: { width: slot.width, height: slot.height },
targeting: slot.targeting,
pageUrl: window.location.href,
referrer: document.referrer
})
});
if (!response.ok) throw new Error('Ad request failed');
return response.json();
}
fireImpression(id) {
const slot = this.slots.get(id);
if (!slot || slot.impressionFired) return;
slot.impressionFired = true;
this.trackerManager.trackImpression(id, {
viewable: true,
format: slot.format
});
slot.callbacks.onImpression?.();
}
unregisterSlot(id) {
const slot = this.slots.get(id);
if (!slot) return;
// Cleanup visibility observer
this.visibilityManager.unobserve(slot.element);
// Cleanup renderer
this.rendererManager.destroy(slot.element);
// Remove from slots
this.slots.delete(id);
}
refresh(ids = []) {
const slotsToRefresh = ids.length
? ids.map(id => this.slots.get(id))
: Array.from(this.slots.values());
slotsToRefresh.forEach(slot => {
if (!slot) return;
// Reset state
slot.state = 'registered';
slot.impressionFired = false;
// Clear current ad
this.rendererManager.destroy(slot.element);
// Reload ad
this.loadAd(slot.id);
});
}
}3. Visibility Manager
class VisibilityManager {
constructor() {
this.observers = new Map();
this.timers = new Map();
}
observe(element, options) {
const { onVisible, onViewable, threshold, duration } = options;
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting && entry.intersectionRatio >= threshold) {
// Element is visible
onVisible?.();
// Start viewability timer
const timer = setTimeout(() => {
onViewable?.();
}, duration);
this.timers.set(element, timer);
} else {
// Element not visible, clear timer
const timer = this.timers.get(element);
if (timer) {
clearTimeout(timer);
this.timers.delete(element);
}
}
},
{
threshold,
rootMargin: '200px' // Lazy load
}
);
observer.observe(element);
this.observers.set(element, observer);
}
unobserve(element) {
const observer = this.observers.get(element);
if (observer) {
observer.disconnect();
this.observers.delete(element);
}
const timer = this.timers.get(element);
if (timer) {
clearTimeout(timer);
this.timers.delete(element);
}
}
}4. Renderer Manager
class RendererManager {
constructor() {
this.renderers = new Map();
}
async render(slot, adData) {
const { format } = slot;
// Load appropriate renderer
const Renderer = await this.getRenderer(format);
// Create renderer instance
const renderer = new Renderer(slot.element, adData);
this.renderers.set(slot.element, renderer);
// Render ad
await renderer.render();
}
async getRenderer(format) {
switch (format) {
case 'video':
const { VideoRenderer } = await import('./renderers/VideoRenderer');
return VideoRenderer;
case 'native':
const { NativeRenderer } = await import('./renderers/NativeRenderer');
return NativeRenderer;
case 'carousel':
const { CarouselRenderer } = await import('./renderers/CarouselRenderer');
return CarouselRenderer;
default:
const { DisplayRenderer } = await import('./renderers/DisplayRenderer');
return DisplayRenderer;
}
}
destroy(element) {
const renderer = this.renderers.get(element);
if (renderer) {
renderer.destroy();
this.renderers.delete(element);
}
}
}
// Example renderer
class DisplayRenderer {
constructor(container, adData) {
this.container = container;
this.adData = adData;
}
async render() {
// Create ad markup
const adElement = document.createElement('div');
adElement.innerHTML = this.adData.creative;
// Attach click handler
adElement.addEventListener('click', () => {
window.open(this.adData.clickUrl, '_blank');
});
// Insert into container
this.container.appendChild(adElement);
}
destroy() {
this.container.innerHTML = '';
}
}5. Tracker Manager
class TrackerManager {
constructor() {
this.eventQueue = [];
this.maxBatchSize = 20;
this.flushInterval = 5000;
this.startAutoFlush();
}
trackImpression(adId, metadata) {
this.track({
type: 'impression',
adId,
timestamp: Date.now(),
...metadata
});
}
trackClick(adId, event) {
const clickData = {
type: 'click',
adId,
coordinates: { x: event.clientX, y: event.clientY },
timestamp: Date.now()
};
// Send immediately with beacon
navigator.sendBeacon(
'/api/ads/track',
JSON.stringify(clickData)
);
}
trackError(adId, error) {
this.track({
type: 'error',
adId,
error: error.message,
timestamp: Date.now()
});
}
track(event) {
this.eventQueue.push(event);
if (this.eventQueue.length >= this.maxBatchSize) {
this.flush();
}
}
async flush() {
if (this.eventQueue.length === 0) return;
const batch = this.eventQueue.splice(0, this.maxBatchSize);
try {
await fetch('/api/ads/track/batch', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(batch),
keepalive: true
});
} catch (error) {
console.error('Tracking failed:', error);
// Store in offline queue
}
}
startAutoFlush() {
setInterval(() => this.flush(), this.flushInterval);
window.addEventListener('beforeunload', () => this.flush());
}
}Error Handling:
class AdErrorHandler {
handleError(error, slot) {
// Log error
console.error(`Ad error in slot ${slot.id}:`, error);
// Track error
this.trackError(slot.id, error);
// Show fallback
this.showFallback(slot);
// Retry logic
this.scheduleRetry(slot);
}
showFallback(slot) {
slot.element.innerHTML = `
<div class="ad-fallback">
Advertisement could not be loaded
</div>
`;
}
scheduleRetry(slot, attempt = 1) {
if (attempt > 3) return; // Max 3 retries
const delay = Math.min(1000 * Math.pow(2, attempt), 10000);
setTimeout(() => {
this.retryLoad(slot, attempt + 1);
}, delay);
}
}Performance Safeguards:
class PerformanceGuard {
constructor() {
this.maxAdsPerPage = 10;
this.maxConcurrentLoads = 3;
this.currentLoads = 0;
this.loadQueue = [];
}
async requestLoad(slot) {
// Check max ads limit
if (this.getTotalAds() >= this.maxAdsPerPage) {
throw new Error('Max ads per page exceeded');
}
// Queue if too many concurrent loads
if (this.currentLoads >= this.maxConcurrentLoads) {
return new Promise((resolve) => {
this.loadQueue.push(() => resolve(this.executeLoad(slot)));
});
}
return this.executeLoad(slot);
}
async executeLoad(slot) {
this.currentLoads++;
try {
await this.loadAd(slot);
} finally {
this.currentLoads--;
// Process queue
if (this.loadQueue.length > 0) {
const next = this.loadQueue.shift();
next();
}
}
}
checkCLS() {
let cls = 0;
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
cls += entry.value;
}
}
if (cls > 0.1) {
console.warn('CLS threshold exceeded:', cls);
// Pause ad loading
this.pauseLoading();
}
}).observe({ type: 'layout-shift', buffered: true });
}
}Performance-Focused:
- "Ads code must be isolated, async, observable, and measurable — because performance directly impacts revenue."
- "We don't block the main thread with ad operations."
- "Fire-and-forget telemetry with retry logic ensures zero data loss."
- "Layout shifts from ads hurt both SEO and user engagement, which directly reduces revenue."
Scalability-Focused:
- "Batching reduces network overhead by 95% compared to individual requests."
- "Dynamic imports ensure we only load code for ad formats that are actually used."
- "IntersectionObserver with lazy loading means ads only request resources when they're about to be viewed."
Quality-Focused:
- "We use viewability tracking because viewable impressions command 2-3x higher CPMs."
- "Fraud detection on the frontend catches obvious bot traffic before it reaches the billing system."
- "Security is layered: sandboxed iframes, CSP headers, and runtime monitoring."
- Isolation: Ads shouldn't affect page performance
- Async Everything: Never block the main thread
- Observable: Track everything (impressions, clicks, errors, performance)
- Measurable: Tie metrics to business outcomes (revenue, engagement)
- Resilient: Handle failures gracefully with fallbacks and retries
- Secure: Treat third-party content as untrusted
- Optimized: Every kilobyte matters when running on every page
Q: How do you handle ad refresh?
"We track time-in-view and user engagement. After 30 seconds of viewability or on scroll events, we can refresh the slot. We clear the old ad, reset the impression flag, and request a new creative. This increases total impressions per page view, boosting revenue while maintaining good UX."
Q: What if the ad server is down?
"We implement exponential backoff with retries, show a fallback message after 3 failed attempts, and track the outage for monitoring. We also cache the last successful ad response as an emergency fallback, though we don't serve stale ads that could hurt advertiser trust."
Q: How do you test ad code?
"Unit tests for tracking logic, integration tests with mock ad servers, visual regression tests for CLS, and real-user monitoring (RUM) to track Core Web Vitals in production. We also do A/B testing on ad loading strategies to measure impact on engagement and revenue."
Q: How do you handle GDPR/privacy?
"We implement consent management—no tracking fires until consent is granted. We use privacy-safe identifiers, respect Do Not Track, and provide clear opt-out mechanisms. All tracking is anonymized and aggregated where possible."
- Revenue Impact Thinking: Always connect technical decisions to business outcomes
- Performance Obsession: Core Web Vitals are table stakes
- Scale Awareness: Solutions must work at millions of requests/sec
- User Experience: Ads shouldn't degrade the page experience
- Security Mindset: Third-party content is inherently risky
❌ "I'd use jQuery for this" ❌ "Just load everything synchronously" ❌ "CLS doesn't really matter" ❌ "We can track impressions on page load" ❌ "I'd use localStorage for tracking" (violates privacy) ❌ Ignoring mobile performance ❌ Not mentioning security concerns ❌ Focusing only on code, not business metrics
"In ad tech, every technical decision has a direct revenue impact. My approach focuses on building isolated, async systems