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

MeasureBefore consent toolAfterTool
LCP p75, mobile1.8 s3.1 sCrUX / web-vitals RUM
LCP p75, desktop1.1 s1.6 sweb-vitals RUM
LCP element, mobile (most common)h1#consent-banner pweb-vitals attribution
Render-blocking requests1 (CSS)3Lighthouse

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.

Sequence of page load with a synchronous consent loader in the head Three actors, the browser, the site and the consent vendor. The browser receives the HTML from the site. It reaches the consent loader script in the head and stops parsing to fetch it from the vendor. The loader fetches the configuration script, also blocking. After both run, parsing resumes, the CSS is applied and the first paint happens 620 milliseconds later than before. How the consent script delayed first paint Browser Site Consent vendor HTML GET loader.js (blocking) GET config.js (blocking) parse resumes, first paint +620 ms
WebPageTest, Moto G Power on Slow 4G; with async loading, first paint returned to its previous time.

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.

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.

Field LCP at the 75th percentile on mobile after each fix Vertical bars. Before the consent tool 1.8 seconds. After adding it 3.1 seconds. With the loader made asynchronous 2.6 seconds. With the banner rendered at build time and shortened 1.9 seconds. With the page overlay removed 1.8 seconds. Mobile LCP after each fix 1.8 s Baseline 3.1 s Tool added 2.6 s Async loader 1.9 s Static banner 1.8 s No overlay
web-vitals RUM, mobile, one week per step; the last value matches the pre-tool baseline.

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.

Page load sequence with a build-time banner and asynchronous consent tool Five steps. The HTML arrives with the banner markup hidden. A one-line inline script shows the banner if no consent cookie exists. The page and banner paint together at first paint. The consent tool loads asynchronously and attaches to the banner's buttons. Analytics and marketing tags load only after the reader accepts. The consent flow after the fixes HTML with banner hidden by default Inline cookie check show if no consent First paint page + banner together Consent tool (async) attaches to buttons Tags after consent gated as before
Nothing that sets cookies or tracks runs before the choice; nothing that renders waits for the vendor.

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

MeasureWith tool, defaultsAfter fixesTool
LCP p75, mobile3.1 s1.8 sweb-vitals RUM
LCP p75, desktop1.6 s1.1 sweb-vitals RUM
Banner as LCP element (mobile first visits)58%9%web-vitals attribution
Render-blocking requests31Lighthouse
Consent rate41%43%Consent tool reports
Non-essential cookies before consent00Playwright 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

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.

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.

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.

Filed underFixes