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: 2024-11-27

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 resizing van afbeeldingen, beeldcompressie, WebP conversie en preloading van het LCP element, kan je Largest Contentful Paint nog steeds niet slagen voor de Core Web Vitals.

De enige manier om dit op te lossen is door geavanceerdere tactieken te gebruiken, zoals 2-stage loading en threading van je pagina met web workers om bronnen vrij te maken op de main thread.

In dit artikel laat ik zien hoe je de Largest Contentful Paint verder kunt verbeteren.

Waarom moet ik de largest contentful paint afbeelding preloaden

Wat achtergrond

Ik ben een pagespeed man 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 misschien niet haalbaar voor je gemiddelde (WordPress) site zonder de ondersteuning van een toegewijd en getalenteerd dev team. Als je deze techniek op je eigen site niet kunt dupliceren, raad ik je toch aan het artikel te lezen en te leren hoe ik over pagespeed denk 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 komt vaak voor dat deze LCP afbeelding niet slaagt voor de Core Web Vitals. Ik zie dagelijks resultaten zoals deze.

slechte LCP met grote afbeelding

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.
  2. Gebruik responsieve afbeeldingen. Zorg ervoor dat je geen afbeeldingen op desktopformaat aan mobiele apparaten serveert.
  3. Comprimeer je afbeeldingen. Beeldcompressie kan de grootte van de afbeelding drastisch verminderen
  4. Gebruik next gen image formats. Next gen image formats 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 bronnen zoals JavaScripts en Style-sheets 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 LCP te vertragen.

De oplossing: 2-stage loading en web workers

De oplossing die ik implementeerde (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 dezelfde exacte afmetingen als de uiteindelijke afbeelding van hoge kwaliteit. Start direct nadat die afbeelding is weergegeven het proces dat de afbeelding van lage kwaliteit verwisselt voor een afbeelding van hoge kwaliteit.

Een heel eenvoudige implementatie zou er ongeveer zo uit kunnen zien: Voeg eerst een load event listener toe aan een afbeelding. Wanneer de afbeelding laadt, maakt diezelfde event listener zichzelf los en wordt de src van de afbeelding verwisseld voor de uiteindelijke, hoge kwaliteit afbeelding.

<img 
     width="100" 
     height="100" 
     alt="een alt tekst" 
     src="lq.webp" 
     onload="this.onload=null;this.src='hq.webp'"
>

Fase 1: lage kwaliteit webp 3-5kb

Fase 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 renderingproces 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 naar een web worker te verplaatsen. 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 gebeurt via een berichtensysteem. Het voor de hand liggende voordeel is dat we de hoofdthread van de pagina zelf niet gebruiken, we maken daar bronnen 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 geladen is, wissel ik hem direct. Als hij niet geladen is (omdat de afbeelding lazy load kan zijn), voeg ik een event listener toe die de afbeelding na lazy load 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 via 2-stage loading en een web worker. De code op de hoofdpagina roept een webworker aan die de afbeeldingen ophaalt. De webworker 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. Het luistert naar berichten. Een bericht bevat een afbeeldings-URL en een unieke afbeeldings-ID. Eerst transformeert het de afbeeldings-URL naar de hoge kwaliteit versie. In mijn geval door /lq te veranderen in /resize in de afbeeldings-URL. De worker haalt dan de hoge kwaliteit afbeelding op, haalt deze op, maakt een blob en stuurt vervolgens de afbeeldings-blob samen met de unieke ID terug.
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

De script.js draait als een normaal script op de actieve webpagina. Het script laadt eerst de worker. Vervolgens loopt het door alle afbeeldingen op een pagina. Dit gebeurt vroeg in het renderingproces. Een afbeelding kan al geladen zijn en misschien niet. Als een afbeelding van lage kwaliteit al is geladen, wordt het wisselproces onmiddellijk aangeroepen. Als het nog niet is geladen, wordt een luisteraar aan het afbeeldingslaadevent gekoppeld die het wisselproces start zodra die afbeelding is geladen..
Wanneer een afbeelding is geladen, wordt een unieke ID gegenereerd voor die afbeelding. Dit stelt me in staat om de afbeelding gemakkelijk weer op de pagina te vinden (onthoud, de worker heeft geen toegang tot de dom, dus ik kan de afbeeldings-DOM Node niet verzenden).
De afbeeldings-URL en unieke ID worden vervolgens naar de worker verzonden.
Wanneer de worker de afbeelding heeft opgehaald, wordt deze als een blob teruggestuurd naar het script. Het script verwisselt uiteindelijk de oude afbeeldings-URL voor de blob-URL die door de web worker is gemaakt.

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 }
                )
        })
})

Core Web Vitals Score met LCP afbeelding gepreload

Search Console flagged your site?

When Google flags your Core Web Vitals you need a clear diagnosis fast. I deliver a prioritized fix list within 48 hours.

Request Urgent Audit
Hoe ik mijn LCP met 70% verlaagdeCore Web Vitals Hoe ik mijn LCP met 70% verlaagde