Fixing LCP Regressions Caused by Cookie Consent Tools
The consent tool went live on Tuesday. By the following week, field LCP on mobile had moved from 1.8 to 3.1 seconds across the whole site, and the Core Web Vitals report in Search Console turned from green to amber. Nothing else had changed. Consent management platforms are one of the most common causes of sudden LCP regressions on static sites, because they are added by a different team, loaded from a third party, and placed — by vendor instructions — as the first script in the <head>, where they can block or delay everything else.
This guide works through a regression on a 600-page Hugo marketing and documentation site after adding a commercial consent management platform, finds three separate mechanisms by which it delayed LCP, and fixes each without changing what loads before consent. It belongs to Largest Contentful Paint Optimization for Static Sites; the CLS side of banners is covered in Fixing CLS from Cookie Banners.
Symptoms
| Measure | Before consent tool | After | Tool |
|---|---|---|---|
| LCP p75, mobile | 1.8 s | 3.1 s | CrUX / web-vitals RUM |
| LCP p75, desktop | 1.1 s | 1.6 s | web-vitals RUM |
| LCP element, mobile (most common) | h1 | #consent-banner p | web-vitals attribution |
| Render-blocking requests | 1 (CSS) | 3 | Lighthouse |
The attribution row was the first clue: on mobile the banner's paragraph had become the LCP element on 58% of first visits.
Mechanism 1: A Synchronous Script in the Head
The vendor's snippet was a plain <script src="https://cdn.consent-vendor.example/loader.js"> at the top of <head>, which loaded a second script (the configuration) synchronously. Both blocked rendering: on a slow 4G profile, 620 ms passed before the browser could parse past them.
Making the loader asynchronous kept compliance intact, because analytics and marketing tags were already gated on the consent tool's callback and could not run before it:
<script async src="https://cdn.consent-vendor.example/loader.js" data-config="acme-prod"></script>
A preconnect to the vendor's domain was added so the script still arrives early, without blocking.
Mechanism 2: The Banner Became the LCP Element
With the script async, first paint returned to normal — but the banner, injected by the script about 1.4 seconds into the load, was now even later. On a 390-pixel-wide phone, its 70-word explanation paragraph painted larger than the page's heading, so it became the LCP element, and LCP was measured at its paint time.
The fix had two parts. First, render the banner's markup at build time as part of the page, hidden by default, and reveal it with a tiny inline script that only checks for an existing consent cookie:
<div id="consent-banner" class="consent" hidden>
<p>We use cookies for analytics if you agree. <a href="/privacy/">Details</a></p>
<button data-consent="accept">Accept</button><button data-consent="reject">Reject</button>
<button data-consent="settings">Settings</button>
</div>
<script>if (!/(?:^|; )consent=/.test(document.cookie)) document.getElementById('consent-banner').hidden = false;</script>
The banner now paints with the page. The vendor script attaches to the existing buttons when it loads, through its "custom UI" mode, and handles choices, storage and tag gating as before.
Second, the banner text was shortened from 70 words to 14, with the details on the privacy page. The banner was no longer the largest text block on most phones, so LCP went back to the heading — and even where the banner remained the LCP element, it now painted at the same time as the rest of the page.
Mechanism 3: The Page Hidden Until Consent
The vendor's default configuration included an "overlay" mode that set body { visibility: hidden } via injected CSS until the tool initialised, to avoid tags running before the decision. On a static site with properly gated tags, that served no purpose and delayed every paint. Turning off the overlay in the vendor dashboard removed it; a check in CI now greps the built HTML and the vendor's injected CSS (fetched in a Playwright run) for visibility: hidden on body and html.
Staying Compliant
None of the changes altered what runs before consent. The test that matters for compliance — that no non-essential cookies are set and no analytics or marketing requests are made before a choice — runs in Playwright on a fresh context:
const requests = [];
page.on('request', (r) => requests.push(r.url()));
await page.goto(url);
await page.waitForTimeout(3000);
expect((await context.cookies()).filter((c) => !ESSENTIAL.includes(c.name))).toEqual([]);
expect(requests.filter((u) => TRACKERS.some((t) => u.includes(t)))).toEqual([]);
It passed before and after the changes. The legal team reviewed the static banner text and the reject button's prominence, which were unchanged in substance.
Working With the Team That Owns the Tool
The consent tool was configured by the legal and marketing teams in the vendor's dashboard, outside the site's repository. That is how the overlay setting had been switched on without anyone in engineering knowing. The fix that lasted was a short shared checklist — script async, custom UI with build-time banner, overlay off, banner text under 20 words — and a Lighthouse CI assertion on the LCP element type, so a dashboard change that brings the regression back fails the next pull request's checks, as in Setting Up Lighthouse CI for a Static Site.
Returning Visitors
Readers who had already made a choice were affected too, though less. The inline cookie check keeps the banner hidden for them from the first frame, so the banner never paints and never competes for LCP. Before the fix, the vendor script had to load and read its own storage before deciding not to show the banner, and on slow connections returning visitors briefly saw the banner flash and disappear — a small layout shift and a confusing experience. The inline check reads the same cookie the vendor writes, so both always agree; a unit test asserts the cookie name and format match the vendor's documentation, because a vendor update that changes the cookie would otherwise show the banner to everyone again.
Measured Impact
| Measure | With tool, defaults | After fixes | Tool |
|---|---|---|---|
| LCP p75, mobile | 3.1 s | 1.8 s | web-vitals RUM |
| LCP p75, desktop | 1.6 s | 1.1 s | web-vitals RUM |
| Banner as LCP element (mobile first visits) | 58% | 9% | web-vitals attribution |
| Render-blocking requests | 3 | 1 | Lighthouse |
| Consent rate | 41% | 43% | Consent tool reports |
| Non-essential cookies before consent | 0 | 0 | Playwright check |
The consent rate did not drop — if anything, a banner that appeared instantly was accepted slightly more often.
Pitfalls & Rollback
- Following the vendor's snippet blindly. Synchronous loading is rarely required for compliance; tag gating is.
- Long banner text. On phones it becomes the LCP element.
- Overlay or anti-flicker modes. They hide the page; turn them off when tags are gated.
- Changing consent behaviour while fixing performance. Keep a test that proves nothing tracks before a choice.
- Rollback: the vendor snippet and dashboard settings can be restored independently; the compliance test should pass either way.
Conclusion
A consent tool regressed this site's mobile LCP from 1.8 to 3.1 seconds through a blocking script, a banner that became the LCP element when injected late, and an overlay that hid the page. Loading the tool asynchronously, rendering a shorter banner at build time and turning off the overlay brought LCP back to 1.8 seconds, with a test proving that nothing tracks before consent.
FAQ
How can a cookie banner affect LCP?
In three ways. A synchronous consent script in the head blocks rendering until it downloads and runs. The banner itself can become the largest element in the viewport on mobile, so LCP is measured when the banner paints, which is late if it is injected by script. And consent tools that hide the page until a choice is made delay every paint.
Can the consent script be loaded asynchronously and stay compliant?
Yes, as long as no tracking or non-essential cookies are set before consent. Compliance depends on what loads before the choice, not on whether the consent script itself is blocking. Load the consent tool asynchronously, keep analytics and marketing tags gated on its consent signal, and the order of loading is preserved.
Why does Lighthouse report the cookie banner as the LCP element?
On a phone-sized viewport, a banner with a long text block can be larger in painted area than the page's heading or first paragraph. LCP counts the largest text block or image in the viewport, so the banner's paragraph wins. If the banner appears late, LCP is late.
Should the banner be rendered at build time on a static site?
Its markup can be. Rendering the banner's HTML and CSS statically, hidden until a tiny inline script checks for an existing consent cookie, makes it paint with the page instead of after a third-party script loads. The consent tool's logic can still run afterwards to handle choices and tag gating.
Related
- Parent: Largest Contentful Paint Optimization for Static Sites — LCP causes and fixes.
- Fixing CLS from Cookie Banners — the layout side of banners.
- Loading Google Tag Manager Without Hurting INP — the tags the consent tool gates.
- Finding the Cause of a Sudden Field LCP Drop — how regressions like this are spotted.
- Privacy-Friendly Analytics Compared by Page Weight — analytics that may not need consent.
Filed underFixes