Skip to content

Optimizing Consent Scripts for Core Web Vitals

Published:

A while back I wrote about how to correctly interpret metrics when consent scripts are involved. The problem was clear: if we monitor performance without considering consent state, we get a distorted view of reality.

Today I want to go one step further. It’s not just about measuring correctly, but about loading the consent script intelligently based on who’s visiting our site.

This article comes from a real discussion on LinkedIn about the Validate Preload on Async/Defer Scripts snippet, where someone asked if using fetchpriority="high" was a valid alternative for CMP scripts. The short answer: it depends. The long answer: this article.

The anti-pattern: preload + async without mitigation

In my performance audits, I’ve seen this hundreds of times:

<!-- ❌ Common anti-pattern -->
<link rel="preload" href="https://cdn.example.com/consent.js" as="script" />
<script async src="https://cdn.example.com/consent.js"></script>

The development team’s intention is reasonable: “I want the script to be discovered early but without blocking rendering”. The problem is that preload elevates the script’s network priority from Lowest/Low to Medium/High, making it compete directly with critical resources like CSS, fonts, or the LCP image.

On limited connections (which are the reality of the 75th percentile of visits), this bandwidth competition can degrade LCP by 100 to 500ms.

What about using fetchpriority="high"?

Even worse. If the script is already being elevated by preload, adding fetchpriority="high" reinforces that high priority. We’re telling the browser: “this script is as important as my CSS or my LCP image”. And in most cases, it’s not.

Mitigation with fetchpriority="low"

If we need the preload for early discovery but want to maintain low priority, the correct combination is:

<!-- ✅ Preload with priority mitigation -->
<link rel="preload" href="https://cdn.example.com/consent.js" as="script" />
<script
  async
  fetchpriority="low"
  src="https://cdn.example.com/consent.js"
></script>

The fetchpriority="low" on the <script> counteracts the preload’s priority elevation, returning the resource to low priority. This gives us the benefit of early discovery without competition with critical resources.

Network priority table

Loading strategyNetwork priorityValid
<script async> onlyLowest/Low
preload + asyncMedium/High❌ Anti-pattern
preload + async + fetchpriority="low"Lowest/Low✅ Mitigated
preload + async + fetchpriority="high"High❌ Worse

We can validate this behavior on our site using the Validate Preload on Async/Defer Scripts snippet, which automatically detects when preload is used on async/defer scripts without the fetchpriority="low" mitigation.

The key question: Do all visits need the CMP with the same urgency?

No.

And this is where it gets interesting. Let’s think about the two real scenarios:

First visit (no consent cookie):

Returning visit (with consent cookie):

This difference is the basis for an optimization that few sites implement: conditional server-side CMP loading.

This is something I’ve seen on many sites: the consent banner is the page’s LCP element.

It’s more common than it seems. Imagine an article page with relatively small text, an image that hasn’t loaded yet, and suddenly a large cookie banner appears with prominent text, occupying a good portion or all of the viewport. That banner becomes the LCP.

In these cases, the CMP script on first visit isn’t just important for functionality, it’s critical for the LCP metric. If the script takes time to load, the banner takes time to appear, and the LCP suffers.

The strategy: Server-Side Conditional Loading

The idea is simple: detect on the server if the consent cookie exists and serve the HTML with the appropriate loading strategy for each case.

Server detection

// Pseudo-code adaptable to any stack
const hasConsentCookie = checkCookie(request, "user_consent");

if (!hasConsentCookie) {
  // First visit - CMP is critical
  renderTemplate({ cmpStrategy: "critical" });
} else {
  // Returning visitor - CMP is not critical
  renderTemplate({ cmpStrategy: "deferred" });
}

Conditional HTML rendering

For first visit, where the CMP is critical:

<head>
  <!-- Option A: Blocking script if the banner IS the LCP -->
  <script src="https://cdn.example.com/consent.js"></script>

  <!-- Option B: Preload + async for non-blocking with early discovery -->
  <link rel="preload" href="https://cdn.example.com/consent.js" as="script" />
  <script async src="https://cdn.example.com/consent.js"></script>
</head>

For returning visits, where the CMP is not critical:

<head>
  <!-- Only async, low priority, no preload -->
  <script async src="https://cdn.example.com/consent.js"></script>
</head>

Implementation with Next.js

Let’s look at a concrete example with Next.js, which with Server Components, gives us native access to cookies on the server:

// app/layout.tsx
import { cookies } from "next/headers";

export default async function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const cookieStore = await cookies();
  const hasConsent = cookieStore.has("user_consent");

  return (
    <html lang="en">
      <head>
        {!hasConsent ? (
          <>
            {/* First visit: preload + async for early discovery */}
            <link
              rel="preload"
              href="https://cdn.example.com/consent.js"
              as="script"
            />
            <script async src="https://cdn.example.com/consent.js" />
          </>
        ) : (
          /* Returning visitor: only async, no preload */
          <script async src="https://cdn.example.com/consent.js" />
        )}
      </head>
      <body>{children}</body>
    </html>
  );
}

If the cookie banner is the LCP element on first visit, consider using a blocking <script> (without async) so the banner appears as soon as possible. Yes, it’s counter-intuitive to prioritize a blocking script, but if that script paints the LCP, it’s the right decision.

Decision table

There’s no one-size-fits-all solution. The strategy depends on each site’s context:

ScenarioRecommended strategyJustification
First visit + CMP is LCPBlocking scriptBanner is critical for UX and is the LCP
First visit + CMP is NOT LCPpreload + async + fetchpriority="low"Early discovery without competing with real LCP
Returning visitor (with cookie)script async without preloadNot critical, minimal overhead
CSR/SPA siteLazy load after LCPCMP can load after first paint

Measurement and validation

As always, any optimization needs data to validate it. It’s not enough to implement and assume it works.

Segmented RUM data

Most important: segment metrics by visit type. If we mix first visit with returning visits in the same dashboard, we’ll be in the same situation I described in Interpret your metrics correctly — data without context.

Chrome DevTools

In the Network tab, verify resource priorities:

In the Performance tab, when doing a Record and Reload, verify that the CMP script doesn’t appear competing in parallel with LCP resources in the Network row.

Validation snippet

For a quick audit, run the Validate Preload on Async/Defer Scripts snippet in the DevTools console. It will show you if there are scripts with the preload anti-pattern without mitigation and what actions to take.

Considerations and trade-offs

I want to be honest about the trade-offs of this strategy:

Additional complexity: Adding server-side logic for conditional loading increases stack complexity. Not all architectures allow it easily (for example, static sites without a server).

CDN cache: If we serve different HTML based on the cookie, we need the cache layer to account for it. The Vary: Cookie header can help, but it also reduces cache effectiveness. Evaluate if the trade-off is worth it in your case.

Third-party CMPs: Many CMP providers have their own loading logic and don’t always allow modifying how the script is inserted. Before implementing this strategy, verify that your provider allows it.

Regulation: Make sure the strategy complies with applicable regulations (GDPR, ePrivacy). Loading the script with low priority on returning visits is fine as long as the script does load and correctly applies preferences.

Conclusion

Consent scripts are a particular case of performance optimization: they’re mandatory but not always critical. The key is understanding that not all visits need the CMP with the same urgency.

With server-side consent cookie detection, we can:

As always, measure with real, segmented data. An optimization without metrics to validate it is just a hypothesis.

References


Previous Post
Yield to Main: setTimeout vs queueMicrotask vs scheduler.postTask
Next Post
Chrome DevTools for Debugging Web Performance