100/100 Core Web Vitals en WordPress
ES

100/100 Core Web Vitals en WordPress

Última verificación: 24 de agosto de 2026
19 min de lectura
Guía
Core Web Vitals
Desarrollador full-stack

100/100 en Lighthouse es un resultado de laboratorio. Los Core Web Vitals que Google usa para posicionar son el p75 del Chrome UX Report en 28 días. Mezcla ambos y persigues una captura de pantalla mientras el campo está en rojo.

WordPress 7.1 sobre PHP 8.4 puede entregar vitals verdes. No lo hace solo. Un tema con Elementor, un banner de cookies que empuja el hero y un botón de Bizum o Redsys que hidrata 200 KB al clic, pierde el INP antes de que el LCP termine. Esta guía tiene dos capas: campo (CrUX), que debe quedar en las bandas verdes de Google, y laboratorio (Lighthouse móvil), donde internamente buscamos 100/100 porque obliga a disciplina en imágenes, CSS y terceros.

El “good” publicado por Google para LCP es 2,5 segundos en el p75 de campo. No ha cambiado en este texto, y no fingimos que un documento de 2026 haya bajado la lista oficial. Cuando decimos que el hero debería pintar cerca de un segundo, es nuestro presupuesto interno de laboratorio, no el umbral de Google. INP “good” es 200 ms. CLS “good” es 0,1. El 100/100 de lab pide números más estrictos porque Lighthouse castiga TTFB, CSS sin usar y trabajo en el hilo principal en una corrida simulada.

El resto del texto habla de consentimiento AEPD/GDPR, widgets de pago habituales en España y del hueco entre un Mac en la oficina y el p75 real en la península y en las islas. Sin eso, una guía de CWV es una traducción, no una herramienta.


#Campo frente a laboratorio: CrUX no es Lighthouse

CrUX es telemetría de campo de usuarios de Chrome que han aceptado el envío de datos. La ventana es de 28 días. La métrica que cuenta es el percentil 75 de la URL o del origen. Lighthouse es laboratorio: un dispositivo, un perfil de red, una carga, a menudo sin login, sin el widget de pago, sin el banner de cookies que solo aparece en el EEE.

Consecuencias:

  • Una página puede tener LCP 1,1 s en lab y 2,8 s en campo porque una parte de los hits llega en 4G al límite de cobertura, o porque el origin se golpea sin cache HTML.
  • Una página puede tener 100 en lab e INP rojo en campo porque el script de Redsys o Klarna solo se inyecta para visitantes de tienda.
  • PageSpeed Insights muestra ambos. Lee primero la pestaña de campo. El lab es diagnóstico, no el veredicto.

Tu propio RUM con web-vitals (onLCP, onINP, onCLS) enviado vía sendBeacon te da p75 antes de que CrUX complete 28 días. Eso es gobierno interno. No sustituye CrUX en Search Console. Nunca informes un número de Lighthouse como si fuera CrUX.

MediciónFuenteQué esQué no es
LCP p75 campoCrUXSeñal de ranking de GoogleUna corrida de Lighthouse
INP p75 campoCrUXTodas las interacciones, 200 ms verdeEl primer clic (eso era FID)
CLS p75 campoCrUXSalto de layout inesperado, 0,1 verde”En el Mac se veía estable”
100/100 móvilLighthouseLab bajo un perfil fijoCampo, y no un umbral de Google

#LCP: los 2,5 s de Google y nuestro presupuesto de lab de 1,2 s

Largest Contentful Paint es el tiempo hasta que el elemento de contenido más grande del viewport se pinta. En WordPress casi siempre es la imagen hero; a veces un H1 sobre una imagen de fondo que nunca llega a ser LCP porque gana el texto.

Google, campo, p75: 2,5 s o mejor es verde. Entre 2,5 y 4,0 s es “needs improvement”. Por encima de 4,0 s es rojo. Esa es la lista. Internamente, en Lighthouse móvil, a menudo buscamos LCP por debajo de 1,2 s porque 100/100 lo exige, y porque deja margen cuando el campo es más duro que el lab. Ese es nuestro objetivo. No es el nuevo umbral de Google.

El tiempo de LCP es TTFB más retraso de carga, más descarga, más retraso de render. Fallos típicos en WordPress:

  1. Hero con loading="lazy". El core y muchos temas hacen lazy de todo. Lazy en la imagen LCP retrasa el inicio de la descarga hasta después del layout. Quita lazy en la primera imagen. Pon fetchpriority="high". Precarga en <head> con imagesrcset e imagesizes que coincidan con lo que realmente se muestra.

  2. WebP donde AVIF ahorra bytes. AVIF es el formato al que codificamos en 2026. WebP es el fallback. JPEG es el original en la mediática, no la entrega. Un hero a 1800 px no necesita un PNG de 3 MB del último export de campaña.

  3. Una fuente que bloquea el pintado. Si el LCP es texto, la webfont está en la ruta crítica. Subconjuntos a los alfabetos que usas (latin + latin-ext para ñ, tildes), font-display: optional o un fallback ajustado por métricas.

  4. Origin sin cache. La precarga no ayuda si el HTML llega a los 700 ms.

Dos casos que medimos:

  • Un catálogo WooCommerce con más de 30 plugins y TTFB alrededor de 1,8 s en plantillas de categoría. Productos relacionados, reseñas, schema y un mega-menú montados en PHP en cada petición. Cache HTML para URLs anónimas de categoría, heroes en AVIF, sin lazy en la primera imagen. El LCP de campo se movió. El checkout siguió sin cachear.
  • Una landing de Elementor que se vino abajo con tráfico de campaña. El builder pintaba cada variante en PHP. No había historia de cache-tags, solo “purgar todo”. No desacoplamos ese sitio. Quitamos el builder, mantuvimos un tema acoplado, Redis y page cache. Desacoplar un stack de page builder es una reescritura editorial, no un cambio de hosting.
<?php
declare(strict_types=1);

add_action('wp_head', static function (): void {
    if (!is_front_page() && !is_singular()) {
        return;
    }
    $url = get_the_post_thumbnail_url(null, 'full');
    if (!$url) {
        return;
    }
    printf(
        '<link rel="preload" as="image" href="%s" fetchpriority="high" />' . "\n",
        esc_url($url)
    );
}, 1);

En Astro 6 y Next.js 16 la precarga vive en la plantilla, no en wp_head. El origin entrega URL y dimensiones vía GraphQL. El frontend escribe <link rel="preload"> e <img> con width, height y un srcset AVIF. No dejes que WordPress y el framework adivinen los dos.


#HTMLRewriter en el edge frente a un plugin de CSS crítico

Esta es la decisión operativa que repetimos: el CSS crítico y las pistas de preload pertenecen al límite HTML, no a otro mu-plugin que corre en cada hit PHP.

Un plugin que “inlinea CSS crítico” sigue necesitando que PHP arranque, consulte el post e imprima un bloque <style>. Eso ayuda al retraso de render después del TTFB. No hace nada por el TTFB. Si el origin está en 600 ms, ya has gastado el presupuesto de LCP antes de que corra el inliner.

Cloudflare HTMLRewriter (o la transformación HTML equivalente en el edge) reescribe un documento cacheado: inyecta preload de la imagen LCP, quita un tracker que marketing metió en wp_footer, añade fetchpriority si la plantilla lo olvidó. El origin PHP no está en ese camino. El GET anónimo sigue siendo cache hit.

Reglas que usamos de verdad:

  1. El HTML anónimo se cachea con tags (post-id, template-home, lang-es). Publicar purga tags, no la zona entera.
  2. HTMLRewriter solo muta pistas seguras e idempotentes. No personaliza. El HTML personalizado es miss por construcción.
  3. Un plugin inliner se tolera en un tema acoplado mientras migras; luego sale. Dos inliners (plugin más edge) duplican bytes y pelean por qué <style> gana.

LiteSpeed Cache más Cloudflare “cache everything” más un inliner son tres caches sin invalidación compartida. El editor guarda, el origin se actualiza, el edge sirve el home de ayer dos horas. Eso no es estrategia de Core Web Vitals. Es cola de soporte.


#INP: 200 ms y el widget de checkout

Interaction to Next Paint sustituyó a FID en 2024. FID medía el primer clic. INP mide clic, toque y tecla a lo largo de toda la sesión. INP verde en campo es 200 ms o mejor en el p75. Entre 200 y 500 ms es “needs improvement”. Por encima de 500 ms es rojo. El 100/100 de lab suele pedir bastante por debajo de 200 ms porque Lighthouse castiga duro las tareas largas.

INP es retraso de entrada más procesamiento más presentación. En tiendas WordPress, el procesamiento casi siempre es un tercero, no tu toggleMenu().

Widgets de pago y mensajería (Redsys, Bizum embebido, Klarna on-site messaging, PayPal, botones Apple Pay) cargan script externo, abren un modal y registran handlers en botones del hero o del carrito. Si el script carga síncrono en <head>, se apropia del hilo principal antes del primer clic en el menú. Carga el widget cuando el checkout está en el viewport (client:visible en Astro 6, import dinámico en Next.js 16), o en idle. El botón puede ser HTML con href a una ruta fina de checkout. Hidrata el widget ahí, no en la home.

Otros:

  • Google Tag Manager, Meta Pixel, Hotjar, chat (Intercom, Crisp, Zendesk): Partytown o carga tras requestIdleCallback. Nunca en la ruta crítica. Partytown no es gratis. Mueve trabajo a un worker. No hace pequeño un contenedor de tags de 400 KB.
  • Listas largas (filtros de tienda, mega-menú): scheduler.yield() entre trozos, o virtualiza. Un querySelectorAll sobre 2000 nodos en un handler de clic es una infracción de INP.
  • Menú móvil: el primer toque en la hamburguesa suele ser el INP que captura CrUX. Si el clic espera a que se hidrate todo el árbol de navegación, los 200 ms se van. Un menú <details> renderizado en servidor, o una isla que solo posee el menú, basta. No hidrates todo el header como una sola isla React.
async function yieldToMain() {
  if ('scheduler' in window && 'yield' in window.scheduler) {
    return window.scheduler.yield();
  }
  return new Promise((resolve) => setTimeout(resolve, 0));
}

Mide INP en campo por plantilla, no como media del origen. Un blog puede estar verde mientras el checkout está rojo. CrUX a nivel de origen lo oculta. Search Console a nivel de URL y tu RUM por page_type lo muestran.


#CLS: banners de consentimiento y pintura tardía del tema

Cumulative Layout Shift suma saltos inesperados. CLS verde en campo es 0,1 o mejor. El 100/100 de lab quiere cerca de 0,00. Las dos fuentes que ganan a un width/height faltante en imágenes son el consentimiento y el tema.

Banner de consentimiento (ePrivacy + GDPR, con la AEPD mirando). Cookiebot, Didomi, OneTrust, Usercentrics y barras caseras suelen inyectarse tras la hidratación. El banner ocupa 80 a 120 px arriba o abajo. El hero, que ya es LCP, se empuja. Eso es CLS, y pega en casi toda primera visita en el EEE. El p75 de CrUX lo registra. Lighthouse sin el banner, no.

Arreglo: reserva el espacio en CSS antes de que corra el script. min-height fijo en un wrapper que siempre está en la plantilla. No animes la altura de 0 a 96 px. Si el banner es overlay, no reserves nada en el flujo, pero bloquea el scroll sin cambiar padding-top de <body> después de medir. Carga el script de consentimiento para que no gane al LCP, pero deja el wrapper en el primer HTML.

Modo oscuro tardío. Si la clase de tema (dark en <html> o <body>) se pone en un script de cliente tras el primer pintado, saltan fondo, texto y sombras. Eso es CLS y a menudo un flash. Arreglo: un <script> síncrono mínimo en <head> antes del CSS, lee localStorage y matchMedia, y mantén variables de color para que el cambio no altere la geometría. Cambia color, no font-size ni line-height. filter: invert en body es un problema de CLS y de contraste, no un tema.

Otro CLS:

  • Imágenes e iframes: width, height o aspect-ratio. Embed de YouTube en caja 16/9.
  • Fuentes: size-adjust, ascent-override en el fallback para que Arial y Geist ocupen la misma caja de línea.
  • Anuncios y bloques relacionados: min-height, no “no sabemos hasta que responda la API”.
.consent-slot {
  min-height: 96px;
}

.hero-media {
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
}

@font-face {
  font-family: Geist-Fallback;
  src: local("Arial");
  size-adjust: 106%;
  ascent-override: 90%;
  descent-override: 22%;
  line-gap-override: 0%;
}

#Imágenes y tipografías sin robar el LCP

La mediática guarda originales. La entrega es AVIF a anchos 400, 768, 1200 y 1920, con sizes que encajan con el layout, no 100vw en un artículo de columna de 680 px. loading="lazy" en todo lo que está bajo el fold. decoding="async". Nada de carrusel en el hero que carga cinco candidatos a LCP.

Fuentes: dos pesos, no cinco. Subconjunto. WOFF2. Autohospedadas, no una CSS de Google Fonts que añade dos rondas extra antes de @font-face. En Astro 6 y Next.js 16 esto vive en el pipeline de build. En un monolito: un plugin o build que escribe ficheros subset, no “Elementor Custom Fonts” con TTF.

El CSS crítico pertenece al primer paquete. Cabecera, tipografía y caja del hero reciben estilo en el HTML, por debajo de unos 14 KB comprimidos, para que el primer pintado no espere a style.css. El resto de la hoja carga async. Builds de Elementor y Divi que mandan 400 KB de CSS a cada URL pierden el LCP antes de mencionar la imagen. En headless, Astro y Next emiten solo los nombres de clase que usa la página. En monolito: extrae above-the-fold por plantilla, o cambia de tema.

content-visibility: auto en comentarios, rejilla de relacionados y pie corta trabajo de estilo y layout en el primer pintado. Pon contain-intrinsic-size para que la barra de scroll no salte (eso es CLS).


#Islas Astro 6 y Next.js 16 para vitals

Dos frentes, mismo requisito: primer pintado sin JavaScript innecesario.

Astro 6 es el default para contenido. HTML y CSS fuera. Islas donde hay clic. client:idle para búsqueda. client:visible para un widget de carrito. Un artículo sin islas manda 0 KB de JS y gana el lab. Por eso 100/100 es alcanzable en un magazine sin hacer trampa al diseño.

Next.js 16 encaja cuando la página es una app. PPR: shell estático, huecos dinámicos. React Server Components para el listado, un client component para el filtro. Todo el App Router como "use client" en el layout es regalar el INP. Partial Prerendering ayuda al LCP. No ayuda si un useEffect en el layout raíz carga Tag Manager y el widget de pago a la vez.

Un monolito en PHP 8.4 con tema ajustado, Redis y Cloudflare también puede ir verde en campo. Pierde cuando el editor instala “un plugincito” de popup. Headless pierde cuando la isla importa todo el pack de iconos. La arquitectura se elige en la guía de arquitectura headless. Los vitals son el resultado de esa arquitectura, más los terceros.

Speculation Rules (<script type="speculationrules"> con prerender conservador en enlaces internos) acerca la siguiente navegación a cero en clientes Chrome compatibles. Mejora la velocidad percibida. No cambia el LCP de un landing desde Google, que es la URL que le importa a CrUX.

Si el front es Astro 6 o Next.js 16 hablando con la API de WordPress, el contrato de vitals es el mismo: shell estático, islas pequeñas, terceros fuera del primer pintado.


#PHP 8.4, Redis y TTFB detrás de Cloudflare

HTML sin cache en hosting compartido es el techo del LCP. Redis object cache corta SQL para usuarios logueados y para misses de cache. Opcache en 8.4 corta el arranque de PHP. Una page cache delante (Cloudflare, o Nginx fastcgi_cache si controlas la VPS) corta PHP por completo para hits anónimos.

Prioridad:

  1. Cache HTML para anónimos, con purge al publicar.
  2. Redis para WP_Query y transients.
  3. Menos plugins en front. Cada wp_enqueue_script es INP.
  4. Índices de base de datos y limpieza de autoload. Autoload de 2 MB en wp_options es TTFB antes de que el tema arranque.

WordPress 7.1 no cambia esto. El core no es el problema de LCP. La cola de scripts y la distancia al origin sí.

Hosting compartido español o un VPS en Madrid sin cache HTML delante sigue dando TTFB de 400 a 800 ms cuando wp-cron y el editor coinciden. Ninguna pipeline AVIF en el tema lo salva. Cloudflare delante (nube naranja) con bypass de /wp-admin, /wp-login.php, preview=true, carrito y cookie wordpress_logged_in es el patrón que mueve el LCP de campo sin convertir el proyecto en un pitch de hosting.


#RUM, presupuestos y regresión

Lab en CI: presupuestos de Lighthouse en PRs para las plantillas que importan (home, artículo, producto, checkout). Falla el build si el JS de la plantilla de artículo supera unas decenas de kilobytes, o si el elemento LCP carece de dimensiones. Eso captura la regresión de un plugin nuevo o de una isla nueva.

Campo: web-vitals a tu endpoint, agregado p75 por plantilla y semana. Alarma cuando el INP cruza 200 ms en campo, no cuando alguien corrió Lighthouse en fibra en la oficina de Madrid. CrUX API semanal como fuente de verdad frente a Search Console.

Presupuestos que sobreviven una semana de campaña:

  • El wrapper de consentimiento tiene altura en CSS.
  • El script de tema en <head> está por debajo de 1 KB y no toca el layout.
  • Los widgets de pago no están en la home.
  • El hero es AVIF, preload, no lazy.

Sin presupuesto, 100/100 es una captura de la semana de lanzamiento.

Multisite no cambia los umbrales. Cambia la carga del origin. Una instancia Redis por red, cache HTML por blog ID, y nada de un all.min.js compartido que carga scripts de WooCommerce en el magazine. Con eso, Multisite puede quedar tan verde en CrUX como una instalación única. Sin eso, cada sitio hereda el enqueue del vecino lento.


#CrUX por país: 4G en la península y en las islas

CrUX no etiqueta “Madrid” frente a “Las Palmas” en el informe que ves en Search Console a nivel de URL. Mezcla el origen. Quien visita desde fibra en Chamberí y quien navega en 4G de Movistar, Orange, Vodafone o Digi en Tenerife, Mallorca o Ibiza entra en el mismo p75. Si el 25 % más lento de esa mezcla vive en red móvil con RTT alto hacia un origin en Frankfurt (o incluso hacia un rack en Madrid sin cache HTML en el edge), el LCP de campo se pone rojo aunque tu Mac en la oficina pinte 1,1 s en Lighthouse.

Lo que medimos en campo, no en un pitch de hosting:

  1. LCP en 4G peninsular frente a islas. El HTML cacheado en Cloudflare (PoP cercano) acorta TTFB en ambos. Sin esa capa, un origin en Frankfurt suma ida y vuelta extra hacia Canarias y Baleares. El hero AVIF con preload sigue siendo necesario, pero no compensa un primer byte de 600 a 900 ms.
  2. INP en el mismo mix. El CPU throttling en Android antiguos es más frecuente en el tramo lento del p75. Un Tag Manager gordo o un script de Redsys síncrono castiga más ahí que en un Pixel reciente en wifi de oficina. El lab con CPU lenta simulada se acerca; el lab “rápido” en el portátil miente.
  3. CLS igual de local. El banner de cookies de la AEPD/GDPR aparece en casi toda primera visita en España. Si solo pruebas con el consentimiento ya aceptado (cookie ya puesta), Lighthouse no ve el empujón del hero. CrUX sí, en península e islas por igual.

Lab desde un Mac en la oficina no es el p75 de CrUX para España. Ni siquiera un PageSpeed Insights “móvil” con perfil 4G es un sustituto del informe de 28 días: no incluye el widget que solo montan los compradores, ni la red real de Digi en una isla, ni el cold cache tras un purge masivo. Usa RUM propio segmentado por effectiveType o por región aproximada si necesitas ver el hueco península/islas antes de que CrUX lo digiera. Luego contrasta con CrUX y Search Console: ellos siguen siendo la fuente de ranking.

Cloudflare con cache HTML anónimo acerca el primer byte al usuario en Madrid y en Las Palmas. Eso es ingeniería de campo, no un argumento comercial de “cambia de proveedor”. El origin puede seguir en Madrid o en Frankfurt; lo que mata el LCP verde es servir cada GET anónimo desde PHP frío a 2000 km del móvil.


#Auditoría y lecturas adicionales

La auditoría de Core Web Vitals es campo más laboratorio más una lista de terceros que deben salir del hilo principal. Abrimos CrUX y Search Console primero, Lighthouse después, y medimos el checkout como plantilla propia.

El trabajo de velocidad sobre un monolito existente está en optimización de velocidad WordPress. El desacoplamiento como topología, no como hechizo de rendimiento, está en la arquitectura headless.


#Conclusión

Google no ha bajado la lista de LCP a 1,2 s. El LCP verde en campo es 2,5 s. El INP verde es 200 ms. El CLS verde es 0,1. 100/100 es disciplina de laboratorio. En WordPress eso significa cache HTML delante del origin, espacio para el banner de consentimiento, tema sin FOUC, widgets de checkout fuera de la primera interacción, y una reescritura en el edge para pistas en lugar de otro inliner PHP. Eso separa el campo verde de una captura bonita de Lighthouse.

Escribe con la URL y acceso a Search Console si el campo está rojo y el lab miente. La optimización de velocidad es la superficie comercial.

Siguiente paso

Transforma el artículo en una implementación real

Este bloque refuerza el enlazado interno y lleva al lector al siguiente paso más útil dentro de la arquitectura del sitio.

¿Quieres implementar esto en tu sitio?

Si el problema está en los Core Web Vitals, en el rendering lento o en el peso de WordPress, puedo mapear e implementar la optimización.

¿Es necesario 100/100 en PageSpeed Insights para SEO en 2026?#
No. Google posiciona con datos de campo de CrUX (p75), no con la puntuación de laboratorio. 100/100 es un objetivo interno de disciplina. El LCP verde en campo es 2,5 s.
¿Qué diferencia hay entre Lighthouse y CrUX?#
Lighthouse es laboratorio: un dispositivo controlado, a menudo 4G simulado, una URL, una ejecución. CrUX es campo: usuarios reales de Chrome durante 28 días. Puedes tener 100 en lab e INP rojo en campo si el widget de Redsys o Bizum solo carga para compradores.
¿Qué suele romper el INP en tiendas WordPress en España?#
Terceros en el hilo principal: plataforma de consentimiento, Tag Manager, Redsys, Bizum, Klarna, chat. Deben salir de la primera interacción.
¿El hosting WordPress gestionado arregla solo el LCP?#
Casi nunca. Workers PHP compartidos y object cache en ficheros hacen oscilar el TTFB. Cache HTML delante del origin, con bypass de wp-admin, es lo que mueve el LCP en campo.
¿Por qué CrUX en Canarias puede verse peor que en Madrid?#
Misma URL, distinta red y distancia al origin. El p75 mezcla península e islas. Un lab desde un Mac en la oficina no representa el 4G en Tenerife o Mallorca si el HTML sale sin cache en el edge.

¿Necesitas un FAQ adaptado a tu sector y mercado? Preparamos una versión alineada con tus objetivos de negocio.

Hablemos

Artículos Relacionados