
Hace un tiempo escribí sobre [cómo interpretar correctamente las métricas](/posts/interpreta-bien-tus-metricas/) cuando hay scripts de consentimiento de por medio. El problema que planteaba era claro: si monitorizamos la performance sin tener en cuenta el estado de los consents, obtenemos una visión distorsionada de la realidad.

Hoy quiero ir un paso más allá. No se trata solo de medir bien, sino de **cargar el script de consentimiento de forma inteligente** según quién visita nuestra web.

Este artículo nace de una [discusión real en 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) sobre el snippet [Validate Preload on Async/Defer Scripts](https://webperf-snippets.nucliweb.net/Loading/Validate-Preload-Async-Defer-Scripts), donde alguien preguntó si usar `fetchpriority="high"` era una alternativa válida para scripts de CMP. La respuesta corta: depende. La respuesta larga: este artículo.

## El anti-patrón: preload + async sin mitigación

En mis auditorías de rendimiento he visto esto cientos de veces:

```html
<!-- ❌ Anti-patrón común -->
<link rel="preload" href="https://cdn.example.com/consent.js" as="script" />
<script async src="https://cdn.example.com/consent.js"></script>
```

La intención del equipo de desarrollo es razonable: _"quiero que el script se descubra pronto pero sin bloquear el renderizado"_. El problema es que `preload` eleva la prioridad de red del script de **Lowest/Low** a **Medium/High**, haciendo que compita directamente con recursos críticos como CSS, fuentes o la imagen LCP.

En conexiones limitadas (que son la realidad del [percentil 75 de visitas](https://developer.chrome.com/docs/crux)), esa competencia por el ancho de banda puede degradar el LCP entre **100 y 500ms**.

### ¿Y si uso `fetchpriority="high"`?

Peor aún. Si el script ya está siendo elevado por `preload`, añadirle `fetchpriority="high"` refuerza esa prioridad alta. Estamos diciéndole al navegador: _"este script es tan importante como mi CSS o mi imagen LCP"_. Y en la mayoría de casos, no lo es.

### La mitigación con `fetchpriority="low"`

Si necesitamos el `preload` para el _early discovery_ pero queremos mantener la prioridad baja, la combinación correcta es:

```html
<!-- ✅ Preload con mitigación de prioridad -->
<link rel="preload" href="https://cdn.example.com/consent.js" as="script" />
<script
  async
  fetchpriority="low"
  src="https://cdn.example.com/consent.js"
></script>
```

El `fetchpriority="low"` en el `<script>` contrarresta la elevación de prioridad del `preload`, devolviendo el recurso a una prioridad baja. Esto nos da el beneficio del _early discovery_ sin la competencia con recursos críticos.

### Tabla de prioridades de red

| Estrategia de carga                          | Prioridad de red | Válida         |
| -------------------------------------------- | ---------------- | -------------- |
| `<script async>` solo                        | Lowest/Low       | ✅             |
| `preload` + `async`                          | Medium/High      | ❌ Anti-patrón |
| `preload` + `async` + `fetchpriority="low"`  | Lowest/Low       | ✅ Mitigado    |
| `preload` + `async` + `fetchpriority="high"` | High             | ❌ Peor        |

Podemos validar este comportamiento en nuestra web usando el snippet [Validate Preload on Async/Defer Scripts](https://webperf-snippets.nucliweb.net/Loading/Validate-Preload-Async-Defer-Scripts), que detecta automáticamente cuándo se usa `preload` en scripts async/defer sin la mitigación de `fetchpriority="low"`.

## La pregunta clave: ¿Todas las visitas necesitan el CMP con la misma urgencia?

No.

Y aquí es donde la cosa se pone interesante. Pensemos en los dos escenarios reales:

**Primera visita (sin cookie de consentimiento):**

- Quien navega **debe** ver el banner para aceptar o rechazar
- El banner se renderiza, ocupa espacio en pantalla, y en muchos casos es el elemento más grande _above the fold_
- El script de CMP es **crítico** para la experiencia

**Visita recurrente (con cookie de consentimiento):**

- Quien navega ya dio o rechazó su consentimiento
- El banner no se mostrará (o solo un pequeño botón flotante para gestionar preferencias)
- El script de CMP **no es crítico**, solo necesita leer la cookie y aplicar las preferencias

Esta diferencia es la base de una optimización que pocas webs implementan: **carga condicional del CMP desde el servidor**.

## Cuando el banner de cookies ES el LCP

Esto es algo que he visto en muchos sitios: **el banner de consentimiento es el elemento LCP de la página**.

Es más común de lo que parece. Imaginad una página de artículo con texto relativamente pequeño, una imagen que aún no ha cargado, y de repente aparece un banner de cookies grande, con texto destacado, ocupando buena parte o todo el viewport. Ese banner se convierte en el LCP.

En estos casos, el script de CMP en primera visita no solo es importante para la funcionalidad, **es crítico para la métrica LCP**. Si el script tarda en cargar, el banner tarda en aparecer, y el LCP se resiente.

## La estrategia: Server-Side Conditional Loading

La idea es sencilla: **detectar en el servidor si existe la cookie de consentimiento y servir el HTML con la estrategia de carga apropiada para cada caso**.

### Detección en servidor

```javascript
// Pseudo-código adaptable a cualquier stack
const hasConsentCookie = checkCookie(request, "user_consent");

if (!hasConsentCookie) {
  // Primera visita - CMP es crítico
  renderTemplate({ cmpStrategy: "critical" });
} else {
  // Returning visitor - CMP no es crítico
  renderTemplate({ cmpStrategy: "deferred" });
}
```

### Renderizado condicional del HTML

Para **primera visita**, donde el CMP es crítico:

```html
<head>
  <!-- Opción A: Blocking script si el banner ES el LCP -->
  <script src="https://cdn.example.com/consent.js"></script>

  <!-- Opción B: Preload + async para non-blocking con discovery temprano -->
  <link rel="preload" href="https://cdn.example.com/consent.js" as="script" />
  <script async src="https://cdn.example.com/consent.js"></script>
</head>
```

Para **visitas recurrentes**, donde el CMP no es crítico:

```html
<head>
  <!-- Solo async, baja prioridad, sin preload -->
  <script async src="https://cdn.example.com/consent.js"></script>
</head>
```

## Implementación con Next.js

Veamos un ejemplo concreto con Next.js, que al tener Server Components, nos da acceso a las cookies en el servidor de forma nativa:

```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="es">
      <head>
        {!hasConsent ? (
          <>
            {/* Primera visita: preload + async para discovery temprano */}
            <link
              rel="preload"
              href="https://cdn.example.com/consent.js"
              as="script"
            />
            <script async src="https://cdn.example.com/consent.js" />
          </>
        ) : (
          /* Returning visitor: solo async, sin preload */
          <script async src="https://cdn.example.com/consent.js" />
        )}
      </head>
      <body>{children}</body>
    </html>
  );
}
```

> Si el banner de cookies es el elemento LCP en primera visita, considerad usar un `<script>` bloqueante (sin `async`) para que el banner aparezca lo antes posible. Sí, es contra-intuitivo priorizar un script bloqueante, pero si ese script pinta el LCP, es la decisión correcta.

## Tabla de decisión

No hay una solución única. La estrategia depende del contexto de cada web:

| Escenario                      | Estrategia recomendada                      | Justificación                                |
| ------------------------------ | ------------------------------------------- | -------------------------------------------- |
| Primera visita + CMP es LCP    | Blocking script                             | El banner es crítico para UX y es el LCP     |
| Primera visita + CMP NO es LCP | `preload` + `async` + `fetchpriority="low"` | Early discovery sin competir con el LCP real |
| Returning visitor (con cookie) | `script async` sin preload                  | No es crítico, mínimo overhead               |
| Site con CSR/SPA               | Lazy load después del LCP                   | El CMP puede cargar después del primer paint |

## Medición y validación

Como siempre, cualquier optimización necesita datos que la validen. No basta con implementar y asumir que funciona.

### RUM data segmentada

Lo más importante: **segmentad las métricas por tipo de visita**. Si mezclamos primera visita con visitas recurrentes en el mismo dashboard, estaremos en la misma situación que describía en [Interpreta bien tus métricas](/posts/interpreta-bien-tus-metricas/) — datos sin contexto.

- Segmentad LCP por primera visita vs recurrente
- Comparad el antes y después de la implementación
- Prestad especial atención al percentil 75, que es el que usa Google para las Core Web Vitals

### Chrome DevTools

En la pestaña **Network**, verificad las prioridades de los recursos:

- Sin preload: el script async debería aparecer con prioridad **Low**
- Con preload sin mitigación: aparecerá como **High** (el anti-patrón)
- Con preload + `fetchpriority="low"`: debería aparecer como **Low**

En la pestaña **Performance**, al hacer un _Record and Reload_, verificad que el script de CMP no aparece compitiendo en paralelo con los recursos del LCP en la fila de Network.

### Snippet de validación

Para una auditoría rápida, ejecutad el snippet [Validate Preload on Async/Defer Scripts](https://webperf-snippets.nucliweb.net/Loading/Validate-Preload-Async-Defer-Scripts) en la consola de las DevTools. Os mostrará si hay scripts con el anti-patrón de preload sin mitigación y qué acciones tomar.

## Consideraciones y trade-offs

Quiero ser honesto con los trade-offs de esta estrategia:

**Complejidad adicional:** Añadir lógica server-side para la carga condicional aumenta la complejidad del stack. No todas las arquitecturas permiten hacerlo fácilmente (por ejemplo, webs estáticas sin servidor).

**Caché de CDN:** Si servimos HTML diferente según la cookie, necesitamos que la capa de caché lo tenga en cuenta. El header `Vary: Cookie` puede ayudar, pero también reduce la eficacia de la caché. Valorad si el trade-off merece la pena en vuestro caso.

**CMPs de terceros:** Muchos proveedores de CMP tienen su propia lógica de carga y no siempre permiten modificar cómo se inserta el script. Antes de implementar esta estrategia, verificad que vuestro proveedor lo permite.

**Regulación:** Aseguraos de que la estrategia cumple con la normativa aplicable (GDPR, ePrivacy). Cargar el script con baja prioridad en visitas recurrentes está bien siempre que el script **sí se cargue** y aplique las preferencias correctamente.

## Conclusión

Los scripts de consentimiento son un caso particular de optimización de rendimiento: son obligatorios pero no siempre críticos. La clave está en entender que **no todas las visitas necesitan el CMP con la misma urgencia**.

Con una detección server-side de la cookie de consentimiento, podemos:

- Priorizar el CMP en primera visita, cuando el banner es visible y puede ser el LCP
- Reducir su impacto en visitas recurrentes, donde el banner no se muestra
- Usar el snippet de validación para detectar anti-patrones de prioridad en producción
- Y claro, esta estrategia no es solo para el CMP: la podéis aplicar a un componente de Onboarding, scripts de Test A/B, Heatmaps, encuestas, etc.

Como siempre, medid con datos reales y segmentados. Una optimización sin métricas que la validen es solo una hipótesis.

## Referencias

- [Validate Preload on Async/Defer Scripts | WebPerf Snippets](https://webperf-snippets.nucliweb.net/Loading/Validate-Preload-Async-Defer-Scripts) — Snippet para detectar el anti-patrón
- [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
- [Interpreta bien tus métricas](/posts/interpreta-bien-tus-metricas/) — El problema de medir sin contexto de consents
