Skip to content

Optimización de scripts de consentimiento para Core Web Vitals

Published:

Hace un tiempo escribí sobre cómo interpretar correctamente las métricas 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 sobre el snippet Validate Preload on 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:

<!-- ❌ 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), 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:

<!-- ✅ 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 cargaPrioridad de redVálida
<script async> soloLowest/Low
preload + asyncMedium/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, 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):

Visita recurrente (con cookie de consentimiento):

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

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

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

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

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

EscenarioEstrategia recomendadaJustificación
Primera visita + CMP es LCPBlocking scriptEl banner es crítico para UX y es el LCP
Primera visita + CMP NO es LCPpreload + async + fetchpriority="low"Early discovery sin competir con el LCP real
Returning visitor (con cookie)script async sin preloadNo es crítico, mínimo overhead
Site con CSR/SPALazy load después del LCPEl 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 — datos sin contexto.

Chrome DevTools

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

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

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

Referencias


Previous Post
Yield to Main: setTimeout vs queueMicrotask vs scheduler.postTask
Next Post
Chrome DevTools para depurar el rendimiento de una web