Cómo reduje mi LCP en un 70%
Aprende métodos avanzados para mejorar los Core Web Vitals

Mejorando las métricas de LCP con Web Workers y carga de imágenes en 2 etapas
La mayoría de las veces, un elemento de imagen grande en el viewport visible se convertirá en el elemento Largest Contentful Paint. Incluso después de aplicar todas las mejores prácticas de Lighthouse como redimensionar imágenes, comprimir imágenes, convertir a WebP y precargar el elemento LCP, tu Largest Contentful Paint aún podría no pasar los Core Web Vitals.
La única forma de solucionarlo es utilizando tácticas más avanzadas como la carga en 2 etapas y ejecutar tu página con Web Workers para liberar recursos en el hilo principal.

Última revisión por Arjen Karel en marzo de 2026
Algo de contexto
Soy un especialista en pagespeed y mi sitio web es mi carta de presentación. En mi página de inicio afirmo con orgullo que mi sitio es el más rápido del mundo. Por eso necesito que mi página cargue lo más rápido posible y exprimir cada gota de rendimiento de mi sitio.
Las técnicas que te mostraré hoy podrían no ser viables para tu sitio promedio (WordPress) sin el apoyo de un equipo de desarrollo dedicado y talentoso. Si no puedes duplicar esta técnica en tu propio sitio, te animo igualmente a leer el artículo y aprender cómo pienso sobre el pagespeed y cuáles son mis consideraciones.
El problema: imágenes grandes en el viewport visible
Una imagen grande en el viewport visible a menudo se convertirá en el elemento Largest Contentful Paint. Frecuentemente ocurre que esta imagen LCP no pasa los Core Web Vitals. Veo resultados como estos a diario.

Hay varias formas de asegurar que este elemento aparezca en pantalla rápidamente:
- Precargar el elemento LCP. Precargar la imagen LCP asegurará que esta imagen esté disponible para el navegador lo antes posible. Combínalo con
fetchpriority="high"para indicarle al navegador que priorice esta imagen sobre otros recursos. - Usa imágenes responsivas. Asegúrate de no estar sirviendo imágenes de tamaño escritorio a dispositivos móviles.
- Comprime tus imágenes. La compresión de imágenes podría reducir drásticamente el tamaño de la imagen.
- Usa formatos de imagen de nueva generación. Los formatos de imagen de nueva generación como WebP superan a los formatos antiguos como JPEG y PNG en casi todos los casos.
- Minimiza la ruta crítica de renderizado. Elimina todos los recursos que bloquean el renderizado como JavaScript y hojas de estilo que podrían retrasar el LCP.
Desafortunadamente, a pesar de todas estas optimizaciones, en algunos casos las métricas de LCP aún podrían no pasar la auditoría de Core Web Vitals. ¿Por qué? El tamaño de la imagen por sí solo es suficiente para retrasar la fase de duración de carga del recurso del LCP.
La solución: carga en 2 etapas y Web Workers
La solución que implementé (después de optimizar todos los demás problemas en mi sitio) es la carga de imágenes en 2 etapas.
La idea es simple: en el primer renderizado mostrar una imagen de baja calidad con las mismas dimensiones exactas que la imagen final de alta calidad. Inmediatamente después de que esa imagen se muestre, iniciar el proceso que intercambia la imagen de baja calidad por una de alta calidad.
Una implementación muy básica podría verse así: Primero añade un event listener de carga a una imagen. Cuando la imagen se carga, ese mismo event listener se desvincula y el src de la imagen se intercambia por la imagen final de alta calidad.
<img
width="100"
height="100"
alt="some alt text"
src="lq.webp"
onload="this.onload=null;this.src='hq.webp'"
>
Etapa 1: webp de baja calidad 3-5kb

Etapa 2: webp de alta calidad 20-40kb

Esto puede parecer suficientemente simple (y lo es), pero intercambiar una gran cantidad de imágenes al inicio del proceso de renderizado causará demasiada actividad en el hilo principal y afectará otras métricas de Core Web Vitals.
Por eso elegí descargar parte del trabajo a un Web Worker. Un Web Worker se ejecuta en un nuevo hilo y no tiene acceso real a la página actual. La comunicación entre el Web Worker y la página se realiza a través de un sistema de mensajería. La ventaja obvia es que no estamos usando el hilo principal de la página; estamos liberando recursos allí. La desventaja es que usar un Web Worker puede ser un poco engorroso.
El proceso en sí no es tan difícil. Una vez que el evento DOMContentLoaded se ha disparado, recolecto todas las imágenes de la página. Si una imagen ya se ha cargado, la intercambiaré inmediatamente. Si no se ha cargado (porque la imagen podría hacer lazy load), adjuntaré un event listener que intercambia la imagen después del lazy load.
Una advertencia importante: el navegador trata cada intercambio de imagen como un nuevo candidato a LCP. Si tu intercambio de imagen de alta calidad ocurre después de 2,5 segundos, el LCP se medirá en el momento del intercambio, no en el momento del placeholder. Por eso es importante que el Web Worker obtenga e intercambie la imagen lo más rápido posible.
El resultado: espectacular.

El código para la carga LCP en 2 etapas a través de un Web Worker
Aquí está el código que uso para acelerar mi LCP mediante la carga en 2 etapas y un Web Worker. El código en la página principal llama a un Web Worker que obtendrá las imágenes. El Web Worker pasa el resultado como un blob a la página principal. Al recibir el blob, la imagen se intercambia.
Worker.js
self.addEventListener('message', async event => {
const newimageURL = event.data.src.replace("/lq-","/resize-");
const response = await fetch(newimageURL)
const blob = await response.blob()
// Send the image data to the UI thread!
self.postMessage({
uid: event.data.uid,
blob: blob,
})
})
Script.js
El script.js se ejecutará como un script normal en la página web activa. El script primero carga el worker. Luego recorrerá todas las imágenes de la página. Esto sucede al inicio del proceso de renderizado. Una imagen puede estar ya cargada o no. Si una imagen de baja calidad ya está cargada, llamará al proceso de intercambio inmediatamente. Si aún no está cargada, adjuntará un listener al evento de carga de la imagen que inicia el proceso de intercambio tan pronto como esa imagen se cargue.
Cuando una imagen se carga, se genera un id único para esa imagen. Esto me permite encontrar fácilmente la imagen en la página de nuevo (recuerda, el worker no tiene acceso al DOM, así que no puedo enviar el nodo DOM de la imagen). La URL de la imagen y el id único se envían luego al worker. Cuando el worker ha obtenido la imagen, se envía de vuelta al script como un blob. El script finalmente intercambia la antigua URL de la imagen por la URL del blob que fue creada por el Web Worker.
var myWorker = new Worker('/path-to/worker.js');
// send a message to worker
const sendMessage = (img) => {
// uid makes it easier to find the image
var uid = create_UID();
// set data-uid on image element
img.dataset.uid = uid;
// send message to worker
myWorker.postMessage({ src: img.src, uid: uid });
};
// generate the uid
const create_UID = () => {
var dt = new Date().getTime();
var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = (new Date().getTime() + Math.random() * 16) % 16 | 0;
dt = Math.floor(dt / 16);
return (c == 'x' ? r : (r & 0x3 | 0x8)).toString(16);
});
return uid;
}
// when we get a result from the worker
myWorker.addEventListener('message', event => {
// Grab the message data from the event
const imageData = event.data
// Get the original element for this image
const imageElement = document.querySelectorAll("img[data-uid='" + imageData.uid + "']");
// We can use the `Blob` as an image source! We just need to convert it
// to an object URL first
const objectURL = URL.createObjectURL(imageData.blob)
// Once the image is loaded, we'll want to do some extra cleanup
imageElement.onload = () => {
URL.revokeObjectURL(objectURL)
}
imageElement[0].setAttribute('src', objectURL)
})
// get all images
document.addEventListener("DOMContentLoaded", () => {
document.querySelectorAll('img[loading="lazy"]').forEach(
img => {
// image is already visible?
img.complete ?
// swap immediately
sendMessage(img) :
// swap on load
img.addEventListener(
"load", i => { sendMessage(img) }, { once: true }
)
})
})
Para verificar la mejora del LCP en el campo, usa Real User Monitoring para rastrear cómo tus visitantes reales experimentan la página. Las herramientas de laboratorio como Lighthouse mostrarán la mejora, pero los datos de campo de usuarios reales en conexiones variadas son lo que cuenta para pasar los Core Web Vitals.
I have done this before at your scale.
Complex platforms, large dev teams, legacy code. I join your team as a specialist, run the performance track, and hand it back in a state you can maintain.
Discuss Your Situation
