TLDR: Cookie consent doesn't have to tank your Lighthouse score. Here's how to add GDPR compliance in 60 seconds with a ~1.6 KB loader that sets Google Consent Mode v2 signals synchronously.
Read full summary
A developer-to-developer deep dive on cookie consent implementation. Covers why most CMPs hurt performance, how Google Consent Mode v2 works, and a solution that respects your bundle size.
Summary by Claude AI
Cookie Consent for Modern Web Developers: Why We Rebuilt It From Scratch
Originally posted on Dev.to
Last month I ran a Lighthouse audit on a client site. Performance score: 68.
The culprit? Their cookie consent banner. Not the banner itself—the 147KB JavaScript bundle it loaded.
For comparison, React is 42KB. Their consent popup was loading 3.5x the size of React. To show a popup. And ask a yes/no question.
This is the state of cookie consent in 2024. Enterprise tools built for marketing teams, deployed on developer sites, destroying Core Web Vitals everywhere they go.
Let me show you a better way.
The Real Problem: Timing
Here's what most developers don't know about Google Consent Mode.
When GTM loads, it checks consent state immediately. If your consent banner hasn't loaded yet—and it probably hasn't, because it's 100KB+ of JavaScript—GTM has no consent signals. It defaults to sending data.
This is what compliant looks like:
// MUST run before GTM
gtag('consent', 'default', {
'ad_storage': 'denied',
'ad_user_data': 'denied',
'ad_personalization': 'denied',
'analytics_storage': 'denied'
});
And this is what most CMPs actually do:
// Runs after DOMContentLoaded
// By then, GTM has already fired
gtag('consent', 'default', { ... }); // Too late
If your consent defaults don't run synchronously in the <head>, they're decorative. GTM doesn't wait.
The Technical Requirements
Here's what a proper consent implementation needs:
1. Synchronous Consent Defaults
Must run in <head> before GTM. Can't be async. Can't wait for DOMContentLoaded.
2. Google Developer ID
Google has a CMP Partner Program. Registered developers get an ID that tells Google "these consent signals are trustworthy":
gtag('set', 'developer_id.dODAxZj', true);
3. Google Consent Mode v2 Signals
Since March 2024, Google requires four specific signals:
| Signal | What It Controls |
|---|---|
ad_storage |
Advertising cookies |
ad_user_data |
Sending user data to Google |
ad_personalization |
Personalized advertising |
analytics_storage |
Analytics cookies |
4. Third-Party Pixel Integration
Meta, TikTok, LinkedIn, Pinterest—they all have their own consent APIs:
// Meta
fbq('consent', 'grant');
fbq('consent', 'revoke');
// TikTok
ttq.enableCookie();
ttq.disableCookie();
// LinkedIn: Nothing. Just block the script.
5. Script Blocking
Scripts marked with type="text/plain" need to be activated after consent:
<script type="text/plain" data-cookieconsent="marketing">
// This shouldn't run until marketing consent
</script>
What I Built
I got tired of explaining to clients why their Lighthouse score dropped after adding "just a cookie banner." So I built one that doesn't suck:
<script src="https://getcookies.co/api/v1/widget/loader.js"
data-domain-id="your-id"
async></script>
How It Works
~1.6 KB synchronous loader:
- Runs immediately in
<head> - Sets consent defaults before GTM loads
- No blocking, no waiting
Async widget:
- Loads the banner UI separately
- Doesn't block rendering
- Zero CLS (positioned fixed from start)
The API
// Check consent state
const hasAnalytics = window.GetCookies.hasConsent('analytics');
const hasMarketing = window.GetCookies.hasConsent('marketing');
// Listen for changes
window.addEventListener('getcookies:consent', (e) => {
const { analytics, marketing, necessary } = e.detail;
if (marketing && !window.myPixelLoaded) {
loadMyPixel();
window.myPixelLoaded = true;
}
});
// Open preferences modal
window.GetCookies.showPreferences();
No callbacks. No jQuery dependencies. Just modern JavaScript.
Framework Integration
Next.js (App Router)
// app/layout.tsx
import Script from 'next/script';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<head>
<Script
src="https://getcookies.co/api/v1/widget/loader.js"
data-domain-id={process.env.NEXT_PUBLIC_GETCOOKIES_ID}
strategy="afterInteractive"
/>
</head>
<body>{children}</body>
</html>
);
}
Nuxt 3
// nuxt.config.ts
export default defineNuxtConfig({
app: {
head: {
script: [{
src: 'https://getcookies.co/api/v1/widget/loader.js',
'data-domain-id': process.env.GETCOOKIES_ID,
async: true
}]
}
}
});
Astro
---
// src/layouts/Layout.astro
---
<html lang="en">
<head>
<script
src="https://getcookies.co/api/v1/widget/loader.js"
data-domain-id={import.meta.env.GETCOOKIES_ID}
async
></script>
</head>
<body>
<slot />
</body>
</html>
Remix
// app/root.tsx
import { Links, Meta, Outlet, Scripts } from '@remix-run/react';
export default function App() {
return (
<html lang="en">
<head>
<Meta />
<Links />
<script
src="https://getcookies.co/api/v1/widget/loader.js"
data-domain-id={ENV.GETCOOKIES_ID}
async
/>
</head>
<body>
<Outlet />
<Scripts />
</body>
</html>
);
}
Bundle Size Comparison
I care about Core Web Vitals. You probably do too. Here's the reality:
| CMP | Bundle Size | LCP Impact | TBT Impact |
|---|---|---|---|
| OneTrust | 80-150KB | Severe | Moderate |
| Cookiebot | 45KB | Noticeable | Low |
| Termly | 35KB | Noticeable | Low |
| CookieYes | 30KB | Noticeable | Low |
| GetCookies | ~1.6 KB (loader) | Negligible | None |
The ~1.6 KB loader sets consent defaults. The banner UI loads async and doesn't block anything.
Handling Third-Party Scripts
Option 1: Data Attributes
<!-- Blocked until marketing consent -->
<script type="text/plain" data-cookieconsent="marketing">
// Meta Pixel code
</script>
<!-- Blocked until analytics consent -->
<script type="text/plain" data-cookieconsent="analytics">
// GA4 code
</script>
Option 2: Event Listener
window.addEventListener('getcookies:consent', (e) => {
if (e.detail.marketing) {
// Load Meta Pixel
loadMetaPixel();
}
if (e.detail.analytics) {
// Load GA4
loadGA4();
}
});
Option 3: Polling (For Already-Loaded Scripts)
// For scripts that load regardless but need consent gating
function initAnalytics() {
if (!window.GetCookies?.hasConsent('analytics')) {
return;
}
// Initialize analytics
gtag('config', 'G-XXXXXXXX');
}
Free Tier
Because side projects shouldn't need a budget:
- 1 domain
- Unlimited page views
- All core features
- Google Consent Mode v2
- Third-party pixel integration
- No credit card required
The Bottom Line
Cookie consent is a solved problem. The industry just solved it with enterprise budgets and marketing team requirements.
For developers who care about performance, who want clean APIs, and who don't want to read 47 pages of Google documentation—there's now a better option.
The code:
<script src="https://getcookies.co/api/v1/widget/loader.js"
data-domain-id="your-id"
async></script>
The result:
- GDPR compliant
- Google Consent Mode v2 configured
- Third-party pixels handled
- Lighthouse score intact
60 seconds. ~1.6 KB. Done.
Try it: getcookies.co
Feedback welcome. What features are missing? What integrations do you need? What did I get wrong?