
A while back I wrote about [how to correctly interpret metrics](/en/blog/interpret-metrics-correctly/) 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](https://www.linkedin.com/feed/update/urn:li:activity:7427372526315679744?commentUrn=urn%3Ali%3Acomment%3A%28activity%3A7427372526315679744%2C7427372619835805696%29&replyUrn=urn%3Ali%3Acomment%3A%28activity%3A7427372526315679744%2C7427662046189502464%29&dashCommentUrn=urn%3Ali%3Afsd_comment%3A%287427372619835805696%2Curn%3Ali%3Aactivity%3A7427372526315679744%29&dashReplyUrn=urn%3Ali%3Afsd_comment%3A%287427662046189502464%2Curn%3Ali%3Aactivity%3A7427372526315679744%29) about the [Validate Preload on Async/Defer Scripts](https://webperf-snippets.nucliweb.net/Loading/Validate-Preload-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:

```html
<!-- ❌ 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](https://developer.chrome.com/docs/crux)), 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:

```html
<!-- ✅ 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 strategy                             | Network priority | Valid          |
| -------------------------------------------- | ---------------- | -------------- |
| `<script async>` only                        | Lowest/Low       | ✅             |
| `preload` + `async`                          | Medium/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](https://webperf-snippets.nucliweb.net/Loading/Validate-Preload-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):**

- Visitors **must** see the banner to accept or reject
- The banner renders, takes up screen space, and in many cases is the largest element _above the fold_
- The CMP script is **critical** for the experience

**Returning visit (with consent cookie):**

- Visitors already gave or denied their consent
- The banner won't be shown (or only a small floating button to manage preferences)
- The CMP script **is not critical**, it only needs to read the cookie and apply preferences

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

## When the cookie banner IS the LCP

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

```javascript
// 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:

```html
<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:

```html
<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:

```tsx
// 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:

| Scenario                       | Recommended strategy                        | Justification                                   |
| ------------------------------ | ------------------------------------------- | ----------------------------------------------- |
| First visit + CMP is LCP       | Blocking script                             | Banner is critical for UX and is the LCP        |
| First visit + CMP is NOT LCP   | `preload` + `async` + `fetchpriority="low"` | Early discovery without competing with real LCP |
| Returning visitor (with cookie)| `script async` without preload              | Not critical, minimal overhead                  |
| CSR/SPA site                   | Lazy load after LCP                         | CMP 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](/en/blog/interpret-metrics-correctly/) — data without context.

- Segment LCP by first visit vs returning
- Compare before and after implementation
- Pay special attention to the 75th percentile, which is what Google uses for Core Web Vitals

### Chrome DevTools

In the **Network** tab, verify resource priorities:

- Without preload: the async script should appear with **Low** priority
- With preload without mitigation: it will appear as **High** (the anti-pattern)
- With preload + `fetchpriority="low"`: it should appear as **Low**

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](https://webperf-snippets.nucliweb.net/Loading/Validate-Preload-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:

- Prioritize the CMP on first visit, when the banner is visible and may be the LCP
- Reduce its impact on returning visits, where the banner isn't shown
- Use the validation snippet to detect priority anti-patterns in production
- And of course, this strategy isn't just for CMPs: you can apply it to Onboarding components, A/B testing scripts, Heatmaps, surveys, etc.

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

## References

- [Validate Preload on Async/Defer Scripts | WebPerf Snippets](https://webperf-snippets.nucliweb.net/Loading/Validate-Preload-Async-Defer-Scripts) — Snippet to detect the anti-pattern
- [JavaScript Loading Priorities in Chrome](https://addyosmani.com/blog/script-priorities/) — Addy Osmani
- [Preload critical assets](https://web.dev/articles/preload-critical-assets) — web.dev
- [Interpret your metrics correctly](/en/blog/interpret-metrics-correctly/) — The problem of measuring without consent context
