Hoe ik mijn LCP met 70% verlaagde

Leer geavanceerde methoden om de Core Web Vitals te verbeteren

Arjen Karel Core Web Vitals Consultant
Arjen Karel - linkedin
Last update: 2026-03-04

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.

Why should I preload the largest contentful paint image

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.

bad LCP with large image

Er zijn een aantal manieren om ervoor te zorgen dat dit element snel op het scherm verschijnt:

  1. 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.
  2. Gebruik responsive afbeeldingen. Zorg ervoor dat je geen desktop-formaat afbeeldingen serveert aan mobiele apparaten.
  3. Comprimeer je afbeeldingen. Beeldcompressie kan de grootte van de afbeelding drastisch verminderen.
  4. Gebruik next gen afbeeldingsformaten. Next gen afbeeldingsformaten zoals WebP presteren in bijna alle gevallen beter dan oudere formaten zoals JPEG en PNG.
  5. 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

De worker heeft één taak. Hij luistert naar berichten. Een bericht bevat een afbeeldings-URL en een uniek id. Eerst transformeert hij de afbeeldings-URL naar de hoge kwaliteit versie. In mijn geval door /lq te veranderen naar /resize in de afbeeldings-URL. De worker haalt vervolgens de afbeelding van hoge kwaliteit op, maakt een blob en retourneert de afbeeldings-blob samen met het unieke id.
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.

About the author

Arjen Karel is a web performance consultant and the creator of CoreDash, a Real User Monitoring platform that tracks Core Web Vitals data across hundreds of sites. He also built the Core Web Vitals Visualizer Chrome extension. He has helped clients achieve passing Core Web Vitals scores on over 925,000 mobile URLs.

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
Hoe ik mijn LCP met 70% verlaagdeCore Web Vitals Hoe ik mijn LCP met 70% verlaagde