Update note (October 23, 2026): this text was written on July 10, 2026, before the AdSense account was submitted and refused (October 2, 2026). Today no ad loads; the loader is injected only if
PUBLIC_ADSENSE_CLIENTexists, andads.tsis a stub that does not calladsbygoogle.push(). The description of loading “after the LCP” describes the intent of the design, not the current behavior. The CLS budget and the fixed height remain valid as a reference.
A static site is not immune to a bad ad. A third-party <script> loaded without care pushes content on the screen, occupies the main thread, and delays the largest visible element on the page, and that holds both for a WordPress blog and for a 100% pre-rendered site like this one. The difference is that, on a static site, there is no “slow server” excuse: if Core Web Vitals comes out bad, the culprit is a frontend decision, period.
Before turning Google AdSense on for this blog, I treated the ad as a budget, not as a bonus. This post is the real record of that budget: three numbers, three technical decisions, and why each one exists.
The three numbers that become a budget
Core Web Vitals has three metrics with a well-defined “good” threshold:
- LCP (Largest Contentful Paint): time until the largest visible element renders. Good is 2.5 seconds or less (web.dev/articles/lcp).
- INP (Interaction to Next Paint): latency between a user interaction (click, tap, key) and the next frame painted on the screen. Good is 200 milliseconds or less (web.dev/articles/inp).
- CLS (Cumulative Layout Shift): the sum of unexpected layout shifts over the life of the page. Good is 0.1 or less (web.dev/articles/cls).
A third-party ad threatens all three at the same time: the script delays the network (LCP), runs heavy JavaScript on the main thread (INP), and, if its space is not reserved before it loads, pushes everything below it when it finally appears (CLS). I treated each threat as a separate decision, not as a generic “performance” problem.
Fixed height, not a minimum, against CLS
The first version of the design used a generic min-height to reserve ad space. That is a subtle mistake: min-height guarantees a floor, but it does not stop the element from growing if the loaded creative is larger than expected, and on top of that it inflates the reserved space beyond the unit’s real format (a leaderboard 90 to 100 pixels tall does not need 280 pixels of reserve).
The decision that remained was to reserve a fixed height per real ad variant: 100 pixels for the mobile leaderboard (90 on desktop), 250 pixels for the rectangle inside the article, and 250 pixels for the sidebar rectangle, always using height, not min-height, on the container. With that, the space exists at exactly the size of the ad before it loads, and it does not change afterward, filled or empty. CLS from the ad becomes, in practice, zero.
.ad-slot--leaderboard { height: var(--ad-height-leaderboard); }
.ad-slot--in-article { height: var(--ad-height-in-article); }
.ad-slot--sidebar { height: var(--ad-height-sidebar); }
Load after the LCP, on demand
The AdSense script loads asynchronously and only after the element that is normally the page’s LCP (the post’s <h1> or the cover image, when one exists): never before it, so it does not compete for network and rendering with what actually decides the metric.
For below-the-fold slots, initialization happens only when the slot is close to entering the viewport, via IntersectionObserver with a margin of about 200 pixels: it loads a little before the user gets there, not only when the element is already visible. That avoids spending network and main-thread budget on an ad the reader might never scroll far enough to see.
A limit of 2 to 3 units per page
Each additional ad unit is more third-party JavaScript competing for the same main thread that processes the reader’s click and tap, which means more risk for INP. That is why this blog’s ceiling is 2 to 3 units per page, not “however many will fit.” On a post page, that means exactly three fixed positions (top, inside the article, sidebar) and no fourth position, even on large screens.
How I check before publishing
None of these decisions is worth anything without an objective way to check whether the budget is being respected. Before this blog has real traffic, I use lab data: Lighthouse or PageSpeed Insights run with a fixed device and network profile, not with the good connection on my own computer. The profile I use is the default mobile profile of those tools: a CPU with 4x throttling and a network simulating a “Slow 4G.” It is a deliberately worse scenario than normal, because that is what separates “it works on my laptop” from “it works for the real reader.”
Each new ad slot goes through that check before it becomes the standard. If the result blows the LCP, INP, or CLS budget, the answer is not to “overlook” the number: it is to remove the slot or reposition it until the budget closes again. After the site has had real traffic for a few weeks, Google Search Console starts showing Core Web Vitals with real field data (CrUX), which confirms whether what the lab predicted matches the real reader’s experience; but that field data only accumulates a sufficient sample after a month of traffic, so it serves to confirm afterward, not to decide before launch.
What I would do differently
Two things I learned while putting this budget together. The first is that “reserved space” only solves CLS if the reserved value matches the real size of the unit being served; testing with the real ad size, not with a generic placeholder, is mandatory before considering the slot ready. The second is that auto ads (Google’s mode that decides on its own where to insert an ad) would probably yield more in the short term, but I gave up that gain on purpose: an unpredictable position is worse CLS control, and I prefer a budget I decide to one the algorithm decides for me. If revenue comes in far below what was expected, that is the first decision I would revisit, not the last.
Sources
- “Good” threshold for LCP: 2.5 s or less — web.dev: Largest Contentful Paint (official documentation), verified on 2026-07-20.
- “Good” threshold for INP: 200 ms or less — web.dev: Interaction to Next Paint (official documentation), verified on 2026-07-20.
- “Good” threshold for CLS: 0.1 or less — web.dev: Cumulative Layout Shift (official documentation), verified on 2026-07-20.



