Reserving Space for Ad and Promo Slots
Content sites that fund themselves with advertising or sponsorships have a structural layout shift problem: the most valuable slots are near the top of the page, and their contents arrive last. An ad request goes out after the page scripts run, the auction takes hundreds of milliseconds, and a creative of unpredictable size lands in a slot that had no size until then. The paragraph the reader just started moves down 250 pixels. Promo banners inserted by marketing scripts and "sponsor" blocks loaded from a CMS at runtime do the same thing on sites without ads.
This guide reserves space for ad, sponsor and promo slots on a static developer blog that runs display ads in three slots and a rotating sponsor block, sizes the reservations from fill data, handles unfilled slots, moves promos that are known in advance into the build, and measures field CLS before and after. It is part of Cumulative Layout Shift Fixes for Static Sites.
Prerequisites
- An ad server or sponsor source that reports creative sizes per slot (Google Ad Manager, a sponsorship CMS, or a simple JSON of sponsors).
- Field CLS with attribution, as in Debugging CLS with the Layout Instability API.
- Control over the slot containers in the site's templates.
Where the Shifts Came From
Field attribution over two weeks on article pages, mobile:
| Largest shift target | Share of poor-CLS views |
|---|---|
| In-content ad slot after first paragraph | 46% |
| Top leaderboard slot | 27% |
| Sponsor block in sidebar (collapses to top on mobile) | 14% |
| Promo banner inserted by marketing script | 9% |
| Other | 4% |
Field CLS at p75 on mobile article pages was 0.27 — well into "poor" — while Lighthouse, which often receives no ad fill in lab conditions, reported 0.02.
Step 1: Size Each Slot From Fill Data
Reserve the space the slot most often needs, per breakpoint. The ad server's delivery report gave the distribution of served creative sizes per slot:
| Slot | Mobile sizes served | Desktop sizes served |
|---|---|---|
| Top leaderboard | 320×50 (61%), 320×100 (39%) | 728×90 (82%), 970×90 (18%) |
| In-content | 300×250 (93%), 336×280 (7%) | 300×250 (71%), 336×280 (29%) |
Reservations: top leaderboard 100 px on mobile, 90 px on desktop; in-content 280 px on both. Then restrict each slot's allowed sizes in the ad server so nothing larger than the reservation can serve — the 336×280 creatives fit the 280 px reservation, while an occasional 300×600 "half page" that had been allowed in the in-content slot was removed from it.
.ad-slot { display: grid; place-items: center; overflow: hidden; background: var(--surface-muted); }
.ad-slot--top { min-height: 100px; }
.ad-slot--inline { min-height: 280px; margin-block: 1.5rem; }
@media (min-width: 48em) { .ad-slot--top { min-height: 90px; } }
.ad-slot::before { content: 'Advertisement'; font-size: .75rem; color: var(--text-muted); grid-area: 1 / 1; align-self: start; }
place-items: center centres smaller creatives in the reserved space instead of letting the slot shrink. The label satisfies ad disclosure and makes the empty space look intentional while the ad loads.
Step 2: Never Collapse an Unfilled Slot
About 11% of in-content slot requests did not fill. The previous implementation collapsed empty slots to zero height to avoid a blank box, which shifted content upward — layout shift counts movement in any direction. Now an unfilled slot shows a house promo sized to the reservation: a link to the newsletter or a popular guide, rendered from a small set of static images at build time. In Google Ad Manager, googletag.pubads().collapseEmptyDivs() must not be called for above-the-fold slots; the slotRenderEnded event with isEmpty: true is where the house promo is inserted into the already-reserved space.
Step 3: Load Slots Below the Fold Lazily, Not Late
Lazy-loading ads further down the page is good for performance, but the reservation must exist from the first paint regardless of when the ad loads. Lazy loading with googletag.pubads().enableLazyLoad({ fetchMarginPercent: 200, renderMarginPercent: 100 }) fetches ads as the reader approaches them; since the slots are already sized, the ad arriving while the reader scrolls towards it moves nothing. Scroll-time shifts are counted in CLS, so reserved space matters just as much for slots below the fold.
Step 4: Move Promos and Sponsors Into the Build
The sponsor block and the promo banner were both inserted at runtime: the sponsor from a small API that rotated sponsors per request, and the banner from a marketing tool's script. Both were known in advance — the sponsor schedule is set weekly and campaigns are planned — so both moved into the build:
- Sponsors live in a data file with start and end dates. The build renders the current sponsor into the HTML; a scheduled daily rebuild rotates it, as in Scheduling Content Publication with Cron-Triggered Builds.
- Promo banners are a frontmatter field on the site config with dates and target sections; the layout renders them statically.
This removed the marketing tool's script entirely, which also helped INP, and made both blocks part of the first frame.
Working With the Ad Operations Team
Most of this work touches settings that belong to whoever manages the ad server, not to the developers. The change that made it stick was a short shared document listing each slot, its reserved size per breakpoint, and the creative sizes allowed in it, with the reason: "in-content slot reserves 280 px; do not allow 300×600 here". When a new campaign needed a larger format, the ad operations team now asked for a new slot with its own reservation rather than enabling the size in an existing one. The field CLS dashboard, grouped by largest shift target, was shared with them too; when an ad-related target reappeared in the top three one month, it traced back to a new sponsor format that had been added without a reservation, and was fixed within a day.
Measured Impact
Field data from web-vitals RUM on mobile article pages, two weeks before and after:
| Measure | Before | After |
|---|---|---|
| CLS p75, mobile articles | 0.27 | 0.04 |
| CLS p75, desktop articles | 0.14 | 0.02 |
| Page views with CLS > 0.25 | 29% | 2% |
| Ad viewability (ad server report) | 58% | 64% |
| Revenue per thousand page views | baseline | +3% |
| Third-party scripts on article pages | 7 | 6 |
Viewability improved slightly because ads no longer appeared after the reader had already scrolled past the point where they would land; publishers often fear that reserving space hurts revenue, and on this site it did the opposite.
Pitfalls & Rollback
- Collapsing empty slots. Upward movement counts as shift; fill with a house promo instead.
- Allowing sizes larger than the reservation. Restrict slot sizes in the ad server to match.
- Reserving only for above-the-fold slots. Scroll-time shifts are counted too.
- Runtime personalization above the fold. If a block must be personalized, reserve its space and swap contents without changing size.
- Rollback: reservations are CSS; removing the
min-heightrules restores the previous behaviour, and the build-time promos can be switched back to runtime scripts if needed.
Conclusion
Ads and promos shift static pages because they arrive after the HTML with sizes the layout did not expect. Reserve each slot's most common size per breakpoint from fill data, restrict slots to sizes that fit, fill empty slots rather than collapsing them, and render anything known in advance at build time. On this blog that took mobile CLS from 0.27 to 0.04 and slightly improved ad performance.
FAQ
How do I reserve space for an ad whose size is not known in advance?
Reserve the most likely size for each breakpoint with min-height on the slot container, based on historical fill data. When a smaller ad fills the slot, centre it in the reserved space; when a larger one would fill it, restrict the allowed sizes for that slot so it cannot exceed the reservation.
What should happen when an ad slot does not fill?
Keep the reserved space and show a house promo or a neutral placeholder, rather than collapsing the slot. Collapsing an unfilled slot above the fold shifts everything below it upward, which counts as layout shift just like expansion.
Are sticky ads a CLS problem?
Sticky or anchored ads that overlay content at the edge of the viewport do not shift other elements and are not counted as layout shift for the page content, but they must not push content when they appear, and they cover content, which has its own usability and policy implications.
Why render promo banners at build time?
A promotion known when the site is built can be written into the HTML, so it occupies its space from the first frame. Inserting it with JavaScript after load, or from a third-party personalization script, is one of the most common causes of layout shift on static sites.
Related
- Parent: Cumulative Layout Shift Fixes for Static Sites — all common CLS causes.
- Fixing CLS from Late-Loading Embeds — the same idea for embeds.
- Fixing CLS from Cookie Banners — another runtime insertion.
- Loading Google Tag Manager Without Hurting INP — the scripts that load ads.
- Measuring Third-Party Cost with Request Blocking — quantifying ad script cost.
Filed underHow-tos