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 redimensionamiento de imágenes, compresión de imágenes, conversión a WebP y precarga del elemento LCP, tu Largest Contentful Paint aún podría no pasar los Core Web Vitals.
La única forma de solucionar esto es usando tácticas más avanzadas como la carga en 2 etapas y ejecutar hilos en tu página con webworkers para liberar recursos en el hilo principal.
En este artículo, mostraré cómo mejorar aún más el Largest Contentful Paint.

Algo de contexto
Soy un experto en velocidad de página y mi sitio web es mi carta de presentación. En mi página de inicio proclamo 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 velocidad 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 a que leas el artículo y aprendas cómo pienso sobre la velocidad de página 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 sucede 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.
- Usar imágenes responsivas. Asegúrate de no servir imágenes de tamaño escritorio a dispositivos móviles.
- Comprimir tus imágenes. La compresión de imágenes podría reducir drásticamente el tamaño de la imagen.
- Usar 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.
- Minimizar la ruta crítica de renderizado. Elimina todos los recursos que bloquean el renderizado como JavaScripts 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 el 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 algo así: Primero añadir un listener de evento load a una imagen. Cuando la imagen carga, ese mismo listener se desvincula y el src de la imagen se intercambia por la imagen final de alta calidad.
<img
width="100"
height="100"
alt="algún texto alternativo"
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 bastante simple (y lo es), pero intercambiar una gran cantidad de imágenes temprano en el proceso de renderizado causará demasiada actividad en el hilo principal y afectará otras métricas de Core Web Vitals.
Es por eso que 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 se ha disparado el evento DomContentLoaded, recopilo 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 cargarse de forma lazy), adjuntaré un listener de evento que intercambia la imagen después de la carga lazy.
El resultado: espectacular

El código para carga de LCP en 2 etapas a través de un web worker
Aquí está el código que uso para acelerar mi LCP a través de carga en 2 etapas y un web worker. El código en la página principal llama a un webworker que buscará las imágenes. El webworker 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 una página. Esto sucede temprano en el proceso de renderizado. Una imagen podría ya estar 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 entonces al worker.
Cuando el worker ha obtenido la imagen, se envía de vuelta al script como un blob. El script finalmente intercambia la URL de la imagen antigua por la URL del blob que fue creado 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 }
)
})
})
Puntuación de Core Web Vitals con imagen LCP precargada
Lab data is not enough.
I analyze your field data to find the edge cases failing your user experience.
- Real User Data
- Edge Case Detection
- UX Focused