Hoe ik mijn LCP met 70% verlaagde
Leer geavanceerde methoden om de Core Web Vitals te verbeteren

De LCP-metrics verbeteren met Web Workers en 2-stage image loading
Meestal wordt een groot afbeeldingselement in de zichtbare viewport het Largest Contentful Paint element. Zelfs na het toepassen van alle Lighthouse best practices zoals het verkleinen van afbeeldingen, beeldcompressie, WebP-conversie en het preloaden van het LCP-element, slaagt je Largest Contentful Paint mogelijk nog steeds niet voor de Core Web Vitals.
De enige manier om dit op te lossen is door meer geavanceerde tactieken te gebruiken zoals 2-stage loading en het threaden van je pagina met Web Workers om resources op de main thread vrij te maken.

Laatst beoordeeld door Arjen Karel in maart 2026
Achtergrond
Ik ben een pagespeed-specialist en mijn website is mijn visitekaartje. Op mijn homepage beweer ik trots dat mijn site de snelste site ter wereld is. Daarom moet mijn pagina zo snel mogelijk laden en elke druppel pagespeed uit mijn site persen.
De technieken die ik je vandaag laat zien zijn mogelijk niet haalbaar voor je gemiddelde (WordPress) site zonder de ondersteuning van een toegewijd en getalenteerd ontwikkelteam. Als je deze techniek niet kunt dupliceren op je eigen site, moedig ik je toch aan om het artikel te lezen en te leren hoe ik over pagespeed nadenk en wat mijn overwegingen zijn.
Het probleem: grote afbeeldingen in de zichtbare viewport
Een grote afbeelding in de zichtbare viewport wordt vaak het Largest Contentful Paint element. Het gebeurt regelmatig dat deze LCP-afbeelding niet slaagt voor de Core Web Vitals. Ik zie dit soort resultaten dagelijks.

Er zijn een aantal manieren om ervoor te zorgen dat dit element snel op het scherm verschijnt:
- Preload het LCP-element. Het preloaden van de LCP-afbeelding zorgt ervoor dat deze afbeelding zo vroeg mogelijk beschikbaar is voor de browser. Combineer dit met
fetchpriority="high"om de browser te vertellen deze afbeelding prioriteit te geven boven andere resources. - Gebruik responsive afbeeldingen. Zorg ervoor dat je geen desktop-formaat afbeeldingen serveert aan mobiele apparaten.
- Comprimeer je afbeeldingen. Beeldcompressie kan de grootte van de afbeelding drastisch verminderen.
- Gebruik next gen afbeeldingsformaten. Next gen afbeeldingsformaten zoals WebP presteren in bijna alle gevallen beter dan oudere formaten zoals JPEG en PNG.
- Minimaliseer het critical rendering path. Elimineer alle render blocking resources zoals JavaScript en stylesheets die de LCP kunnen vertragen.
Helaas, ondanks al deze optimalisaties, slagen de LCP-metrics in sommige gevallen nog steeds niet voor de Core Web Vitals audit. Waarom? De grootte van de afbeelding alleen is genoeg om de resource load duration fase van LCP te vertragen.
De oplossing: 2-stage loading en Web Workers
De oplossing die ik heb geïmplementeerd (na het optimaliseren van alle andere problemen op mijn site) is 2-stage image loading.
Het idee is simpel: toon bij de eerste render een afbeelding van lage kwaliteit met exact dezelfde afmetingen als de uiteindelijke afbeelding van hoge kwaliteit. Onmiddellijk daarna start het proces dat de afbeelding van lage kwaliteit verwisselt voor een afbeelding van hoge kwaliteit.
Een zeer basale implementatie zou er ongeveer zo uit kunnen zien: Voeg eerst een load event listener toe aan een afbeelding. Wanneer de afbeelding laadt, ontkoppelt diezelfde event listener zichzelf en wordt de src van de afbeelding verwisseld voor de uiteindelijke afbeelding van hoge kwaliteit.
<img
width="100"
height="100"
alt="some alt text"
src="lq.webp"
onload="this.onload=null;this.src='hq.webp'"
>
Stage 1: lage kwaliteit webp 3-5kb

Stage 2: hoge kwaliteit webp 20-40kb

Dit lijkt misschien simpel genoeg (en dat is het ook), maar het verwisselen van een groot aantal afbeeldingen vroeg in het renderproces zal te veel activiteit op de main thread veroorzaken en andere Core Web Vitals metrics beïnvloeden.
Daarom heb ik ervoor gekozen om een deel van het werk af te laden naar een Web Worker. Een Web Worker draait in een nieuwe thread en heeft geen echte toegang tot de huidige pagina. Communicatie tussen de Web Worker en de pagina verloopt via een berichtensysteem. Het duidelijke voordeel is dat we niet de main thread van de pagina zelf gebruiken; we maken daar resources vrij. Het nadeel is dat het gebruik van een Web Worker een beetje omslachtig kan zijn.
Het proces zelf is niet zo moeilijk. Zodra het DOMContentLoaded event is afgevuurd, verzamel ik alle afbeeldingen op de pagina. Als een afbeelding al is geladen, verwissel ik deze onmiddellijk. Als deze nog niet is geladen (omdat de afbeelding mogelijk lazy loadt), koppel ik een event listener die de afbeelding verwisselt na lazy load.
Een belangrijke kanttekening: de browser behandelt elke afbeeldingsvervanging als een nieuwe LCP-kandidaat. Als je high-quality afbeeldingsvervanging na 2,5 seconden plaatsvindt, wordt de LCP gemeten op het moment van de vervanging, niet op het moment van de placeholder. Daarom is het belangrijk dat de Web Worker de afbeelding zo snel mogelijk ophaalt en verwisselt.
Het resultaat: spectaculair.

De code voor 2-stage LCP loading via een Web Worker
Hier is de code die ik gebruik om mijn LCP te versnellen door middel van 2-stage loading en een Web Worker. De code op de hoofdpagina roept een Web Worker aan die de afbeeldingen zal ophalen. De Web Worker geeft het resultaat als een blob door aan de hoofdpagina. Bij ontvangst van de blob wordt de afbeelding verwisseld.
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
Het script.js draait als een normaal script op de actieve webpagina. Het script laadt eerst de worker. Vervolgens doorloopt het alle afbeeldingen op een pagina. Dit gebeurt vroeg in het renderproces. Een afbeelding is mogelijk al geladen of nog niet. Als een afbeelding van lage kwaliteit al is geladen, wordt het verwisselproces onmiddellijk gestart. Als deze nog niet is geladen, wordt een listener gekoppeld aan het image load event dat het verwisselproces start zodra die afbeelding is geladen.
Wanneer een afbeelding is geladen, wordt een uniek id gegenereerd voor die afbeelding. Dit maakt het gemakkelijk om de afbeelding op de pagina terug te vinden (onthoud, de worker heeft geen toegang tot de DOM, dus ik kan de afbeeldings DOM Node niet verzenden). De afbeeldings-URL en het unieke id worden vervolgens naar de worker gestuurd. Wanneer de worker de afbeelding heeft opgehaald, wordt deze als blob teruggestuurd naar het script. Het script verwisselt uiteindelijk de oude afbeeldings-URL voor de blob-URL die door de Web Worker is aangemaakt.
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 }
)
})
})
Om de LCP-verbetering in het veld te verifiëren, gebruik Real User Monitoring om te volgen hoe je daadwerkelijke bezoekers de pagina ervaren. Labtools zoals Lighthouse tonen de verbetering, maar velddata van echte gebruikers op variërende verbindingen is wat telt voor het halen van de Core Web Vitals.
Your Lighthouse score is not the full picture.
Lab tests run on fast hardware with a stable connection. I analyze what your actual visitors experience on real devices and real networks.
Analyze Field Data
