How to Integrate Programmatic Ads into Next.js Without Sacrificing Core Web Vitals

Monetising a web application with programmatic advertising can turn a publishing product into a sustainable business.
But there is a catch.
If you integrate advertising the same way you would add any other third-party JavaScript library—by loading large scripts as early as possible, requesting every ad immediately, and allowing ad creatives to determine their own dimensions—you can turn monetisation into a performance problem.
The result can be slower pages, unstable layouts, unnecessary ad requests and a poorer user experience.
For publishers, this creates an uncomfortable trade-off:
You want more monetizable inventory, but you don’t want advertising to compete with your application’s critical rendering path.
The solution isn’t to remove ads.
It’s to engineer the ad stack properly.
I have worked in publisher monetisation as an advertising technology and operations professional. In this article, I presents a practical framework for integrating programmatic advertising into modern Next.js and React applications while protecting Core Web Vitals and maintaining a healthy monetisation strategy.
The framework is:
Reserve → Prioritize → Fetch → Render → Measure
Why Ad Integrations Can Hurt Web Performance
Advertising introduces third-party code, network requests, auctions, creative downloads and iframe rendering into your application.
If these activities happen at the wrong time, they can compete with the resources required to render and make your application interactive.
There are four common problems to watch for.
1. Third-party JavaScript competes for critical resources
Ad technology can involve multiple network requests and JavaScript execution.
Loading third-party scripts too early can compete with the resources your application needs to render the initial experience.
This is particularly important for metrics such as Largest Contentful Paint (LCP) and Interaction to Next Paint (INP), which are part of the current Core Web Vitals set.
The objective isn’t necessarily to prevent advertising scripts from loading.
It’s to make sure they don’t unnecessarily compete with critical application resources.
2. Ads can cause layout instability
Consider this sequence:
Page loads
↓
Article content renders
↓
Ad container has 0px height
↓
Ad auction completes
↓
Creative renders
↓
Content moves downward
That movement contributes to Cumulative Layout Shift (CLS).
The browser should know approximately how much space an ad is going to occupy before the creative renders.
3. You can request ads that users never see
Imagine an article containing six ad slots.
A user lands at the top, reads the first paragraph and leaves.
If all six slots were immediately requested, the application may have initiated work for inventory that the user never had an opportunity to view.
That’s unnecessary network and auction activity.
Lazy loading allows publishers to delay requests for inventory that is further away from the user’s viewport.
4. Not every ad slot deserves the same priority
This is an important distinction.
A top-of-page ad and a bottom-of-article ad shouldn’t necessarily be treated identically.
For example:
| Ad position | Typical strategy |
|---|---|
| Top-of-page | Prioritize |
| Near the beginning of content | Load relatively early |
| Mid-article | Lazy load |
| Sidebar below the fold | Lazy load |
| Bottom of article | Aggressively defer |
The goal isn’t:
Lazy-load every ad.
The goal is:
Load valuable inventory early enough to monetize it, while deferring inventory users may never see.
The 5-Step Framework
A performant advertising architecture can be thought of as five stages:
RESERVE
↓
PRIORITIZE
↓
FETCH
↓
RENDER
↓
MEASURE
Let’s look at each one.
1. Reserve Space for Your Ad Slots
The first rule is simple:
Don’t allow an ad slot to start at zero height if you already know the space the ad is expected to occupy.
For fixed-size inventory, explicitly reserve the dimensions.
For example:
// components/AdSlot.tsx
interface AdSlotProps {
slotId: string;
width: number;
height: number;
}
export function AdSlot({ slotId, width, height, }: AdSlotProps) {
return (
<div
id={slotId}
className="ad-container my-4 flex items-center justify-center"
style={{ minWidth: `${width}px`, minHeight: `${height}px`, }}
aria-label="Advertisement"
/>
);
}
If you’re using a 300 × 250 unit, for example, the browser can reserve that area before the creative arrives.
<AdSlot
slotId="article-sidebar-1"
width={300}
height={250}
/>
What about responsive ads?
Not every ad slot has a single fixed size.
Responsive inventory may have different eligible sizes depending on viewport width and device.
In those cases, don’t blindly use a fixed height everywhere.
Instead, design the slot so that its expected layout space is reserved across your supported breakpoints.
For example:
.ad-container {
width: 100%;
min-height: 250px;
}
@media (min-width: 768px) {
.ad-container {
min-height: 280px;
}
}
The principle remains the same:
Reserve the layout space before the ad creative arrives.
2. Prioritize Your Ad Inventory
The next question isn’t:
“How do I load every ad?”
It’s:
“Which ads should I load first?”
Your highest-priority inventory is generally the inventory closest to the initial user experience.
For example:
PAGE LOAD
│
▼
┌─────────────────┐
│ Above-the-fold │
│ ad │
└────────┬────────┘
│
Load early
│
▼
┌─────────────────┐
│ Near viewport │
│ ads │
└────────┬────────┘
│
Lazy load
│
▼
┌─────────────────┐
│ Far below fold │
│ ads │
└─────────────────┘
This approach creates a better balance between monetization opportunity and resource consumption.
It also prevents a common mistake: treating every ad slot as equally important.
3. Load the Ad Technology Without Blocking the Application
If you’re using Google Publisher Tag (GPT), Next.js provides a better mechanism for loading the external script than manually inserting a raw <script> tag.
Next.js’s Script component lets you control when third-party JavaScript is fetched and executed. For non-critical third-party scripts, Next.js documents afterInteractive and lazyOnload as strategies that can defer execution and reduce interference with the initial page experience.
For example:
// app/layout.tsx
import Script from "next/script";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
{children}
<Script
src="https://securepubads.g.doubleclick.net/tag/js/gpt.js"
strategy="lazyOnload"
/>
</body>
</html>
);
}
This tells Next.js to load the third-party script lazily during browser idle time rather than treating it as part of the application’s critical rendering path.
But should GPT always use lazyOnload?
Not necessarily.
This is an optimization decision rather than a universal rule.
If you delay GPT too aggressively, you may also delay your auction and ad rendering.
That creates a trade-off:
Earlier GPT initialization
↓
Earlier ad request
↓
More time for auction + creative
versus:
Later GPT initialization
↓
Less competition during initial page load
↓
Potentially better application performance
The right configuration depends on your page, inventory, demand stack and performance targets.
Measure it rather than assuming one strategy is always optimal.
4. Use GPT’s Native Lazy Loading Where Appropriate
If you’re using Google Publisher Tag, you don’t necessarily need to build your entire ad lazy-loading system around IntersectionObserver.
GPT provides native lazy-loading controls that separate when an ad is fetched from when it is rendered.
For example:
// GPT configuration
window.googletag = window.googletag || {
cmd: [],
};
window.googletag.cmd.push(() => {
window.googletag.setConfig({
lazyLoad: {
fetchMarginPercent: 500,
renderMarginPercent: 200,
mobileScaling: 2.0,
},
});
});
In this example:
fetchMarginPercent: 500means GPT can fetch an ad when the slot is within approximately five viewport heights.renderMarginPercent: 200means GPT can render it when it is within approximately two viewport heights.mobileScaling: 2.0increases those margins on mobile.
This creates a useful two-stage process:
AD SLOT
│
▼
┌──────────────────┐
│ Outside fetch │
│ margin │
└────────┬─────────┘
│
│ approach
▼
┌──────────────────┐
│ FETCH │
│ auction/request │
└────────┬─────────┘
│
│ get closer
▼
┌──────────────────┐
│ RENDER │
│ creative │
└──────────────────┘
This is more flexible than a single “is the element visible?” trigger because it gives the ad stack time to fetch the auction result before the creative actually needs to appear.
Google’s current GPT API supports fetchMarginPercent, renderMarginPercent, and mobileScaling specifically for this purpose.
5. Define and Display Your GPT Slots Correctly
Your React component should be responsible for rendering the slot container, while GPT handles the advertising lifecycle.
A simplified GPT setup could look like this:
// components/GptAd.tsx
"use client";
import { useEffect } from "react";
interface GptAdProps {
slotId: string;
adUnitPath: string;
sizes: googletag.GeneralSize;
}
export function GptAd({
slotId,
adUnitPath,
sizes,
}: GptAdProps) {
useEffect(() => {
window.googletag = window.googletag || {
cmd: [],
};
window.googletag.cmd.push(() => {
const slot = window.googletag
.defineSlot(
adUnitPath,
sizes,
slotId
)
?.addService(
window.googletag.pubads()
);
if (!slot) {
return;
}
window.googletag.display(slotId);
});
}, [slotId, adUnitPath, sizes]);
return (
<div
id={slotId}
className="ad-container flex min-h-[250px] items-center justify-center"
aria-label="Advertisement"
/>
);
}
You can then render it inside your page:
You can then render it inside your page:
<GptAd
slotId="article-ad-1"
adUnitPath="/123456789/example/article"
sizes={[300, 250]}
/>
For production implementations, you should also account for slot lifecycle management, responsive sizes, route changes, duplicate slot definitions and cleanup where your application architecture requires it.
The important architectural separation is:
React
│
├── Renders the slot container
│
└── Controls application lifecycle
│
▼
GPT
│
├── Defines slot
├── Requests ad
├── Runs auction
└── Renders creative
A Note About IntersectionObserver
IntersectionObserver is still useful.
It becomes particularly valuable when you need application-level control over when an advertising component should initialize.
For example, you might have a custom advertising wrapper that shouldn’t even initialize until the component approaches the viewport.
A reusable hook could look like this:
// hooks/useNearViewport.ts
"use client";
import {
useEffect,
useRef,
useState,
} from "react";
export function useNearViewport(
rootMargin = "300px"
) {
const containerRef =
useRef<HTMLDivElement | null>(null);
const [isNearViewport, setIsNearViewport] =
useState(false);
useEffect(() => {
const element = containerRef.current;
if (!element) {
return;
}
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsNearViewport(true);
observer.disconnect();
}
},
{
rootMargin,
}
);
observer.observe(element);
return () => observer.disconnect();
}, [rootMargin]);
return {
containerRef,
isNearViewport,
};
}
The important distinction is:
Use IntersectionObserver when your application needs to control component initialization. Use GPT’s native lazy-loading configuration when GPT itself can manage the ad fetch/render lifecycle.
That avoids unnecessarily rebuilding functionality already provided by the ad platform.
6. Measure the Result
A monetization implementation isn’t finished when the ad appears.
You need to measure whether the architecture is actually improving the system.
At minimum, monitor both web performance and advertising performance.
Web performance
Track:
- LCP — loading performance
- CLS — layout stability
- INP — interaction responsiveness
These are the current Core Web Vitals used to evaluate key aspects of user experience.
Advertising performance
Track:
- Ad requests
- Fill rate
- Viewability
- Ad latency
- CPM
- Page RPM
- Revenue per session
- Viewable impressions
This matters because a technically faster ad implementation isn’t automatically a commercially better implementation.
For example:
Optimization A
Page performance ↑
Ad requests ↓
Viewability ↓
Revenue ↓
Not necessarily a win.
Compare that with:
Optimization B
LCP ↑
CLS ↓
INP ↑
Viewability ↑
Revenue/session ↑
Much stronger outcome.
The objective is to optimize the whole system, not one metric in isolation.
The Architecture to Aim For
A mature advertising architecture should look something like this:
USER VISITS PAGE
│
▼
┌────────────────────┐
│ Render application │
│ content first │
└──────────┬─────────┘
│
▼
┌────────────────────┐
│ Reserve ad space │
│ to protect CLS │
└──────────┬─────────┘
│
▼
┌────────────────────┐
│ Prioritize slots │
│ by position/value │
└──────────┬─────────┘
│
▼
┌────────────────────┐
│ Slot approaches │
│ viewport │
└──────────┬─────────┘
│
▼
┌────────────────────┐
│ Fetch / auction │
└──────────┬─────────┘
│
▼
┌────────────────────┐
│ Render creative │
└──────────┬─────────┘
│
▼
┌────────────────────┐
│ Measure UX + │
│ monetization │
└────────────────────┘
This is the fundamental principle:
Don’t make your application wait for advertising. Make advertising work around the application’s critical path.
What This Means for Publishers
Programmatic monetization and web performance don’t have to be competing objectives.
The mistake is treating advertising as an isolated snippet of JavaScript:
"Put this tag on the page."
Modern publisher engineering requires a broader view.
You need to consider:
- When the ad technology loads
- Which slots are important
- When an ad request is made
- When the creative is rendered
- How much space the creative requires
- How much work happens on the main thread
- Whether the user is likely to see the inventory
- Whether the resulting inventory actually generates revenue
The best implementation isn’t necessarily the one that loads the most ads or the one that delays every ad as aggressively as possible.
It’s the one that finds the right balance between:
Performance + Viewability + Demand + Revenue + User Experience.
Final Takeaway
If you’re integrating programmatic advertising into a Next.js or React application, don’t start with:
“Where do I paste the ad tag?”
Start with:
“How should this ad participate in the lifecycle of my application?”
Then follow the framework:
1. Reserve
Reserve the space your ad needs to minimize layout instability.
2. Prioritize
Treat above-the-fold and below-the-fold inventory differently.
3. Fetch
Request ads early enough to give the auction time to complete, but avoid unnecessary requests for inventory users may never reach.
4. Render
Render the creative when it is close enough to the viewport to have a realistic opportunity to become viewable.
5. Measure
Evaluate Core Web Vitals and monetization metrics together.
That’s how you build an advertising stack that supports the application instead of fighting it.
Monetization and performance engineering don’t have to be at odds. When your ad stack is engineered with the same discipline as the rest of your application, you can create a better experience for users while building more sustainable advertising inventory.
Need help auditing your ad architecture?
I work at the intersection of ad technology, digital monetization and software engineering.
If you’re building or optimizing a publisher platform, web application or advertising stack and want to understand where your implementation is losing performance or monetization opportunities, you can schedule a discovery call with me via this link: