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 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 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
// 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>(withoutasync) 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 — 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 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 — Snippet to detect the anti-pattern
- JavaScript Loading Priorities in Chrome — Addy Osmani
- Preload critical assets — web.dev
- Interpret your metrics correctly — The problem of measuring without consent context