Wie ich meinen LCP um 70% gesenkt habe

Lernen Sie fortgeschrittene Methoden zur Verbesserung der Core Web Vitals

Arjen Karel Core Web Vitals Consultant
Arjen Karel - linkedin
Last update: 2024-11-27

Verbesserung der LCP-Metriken mit Web-Workern und 2-Stage Image Loading

Meistens wird eine große Bilddatei im sichtbaren Viewport zum Largest Contentful Paint Element. Selbst nach Anwendung aller Lighthouse Best-Practices wie Bildgrößenanpassung, Bildkomprimierung, WebP-Konvertierung und Preloading des LCP-Elements besteht Ihr Largest Contentful Paint möglicherweise immer noch nicht die Core Web Vitals.

Die einzige Möglichkeit, dies zu beheben, besteht in der Verwendung fortgeschrittenerer Taktiken wie 2-Stage Loading und Threading Ihrer Seite mit Web Workern, um Ressourcen im Main Thread freizugeben.

In diesem Artikel zeige ich, wie Sie den Largest Contentful Paint weiter verbessern können.

Warum sollte ich das Largest Contentful Paint Bild preloaden

Ein wenig Hintergrund

Ich bin ein PageSpeed-Typ und meine Website ist mein Aushängeschild. Auf meiner Homepage behaupte ich stolz, dass meine Seite die schnellste Seite der Welt ist. Deshalb muss meine Seite so schnell wie möglich laden und jeden Tropfen PageSpeed aus meiner Seite herausholen.

Die Techniken, die ich Ihnen heute zeige, sind für Ihre durchschnittliche (WordPress) Seite ohne die Unterstützung eines engagierten und talentierten Dev-Teams möglicherweise nicht machbar. Wenn Sie diese Technik auf Ihrer eigenen Seite nicht duplizieren können, ermutige ich Sie dennoch, den Artikel zu lesen und zu erfahren, wie ich über PageSpeed denke und was meine Überlegungen sind.

Das Problem: große Bilder im sichtbaren Viewport

Ein großes Bild im sichtbaren Viewport wird oft zum Largest Contentful Paint Element. Es kommt oft vor, dass dieses LCP-Bild die Core Web Vitals nicht besteht. Ich sehe Ergebnisse wie diese täglich.

Schlechter LCP mit großem Bild

Es gibt eine Reihe von Möglichkeiten, sicherzustellen, dass dieses Element schnell auf dem Bildschirm erscheint:

  1. Preloaden Sie das LCP-Element. Das Preloaden des LCP-Bildes stellt sicher, dass dieses Bild dem Browser so früh wie möglich zur Verfügung steht.
  2. Verwenden Sie responsive Bilder. Stellen Sie sicher, dass Sie keine Bilder in Desktop-Größe an mobile Geräte ausliefern.
  3. Komprimieren Sie Ihre Bilder. Bildkomprimierung kann die Größe des Bildes drastisch reduzieren
  4. Verwenden Sie Next Gen Image Formate. Next Gen Image Formate wie WebP übertreffen ältere Formate wie JPEG und PNG in fast allen Fällen.
  5. Minimieren Sie den Critical Rendering Path. Eliminieren Sie alle Render-Blocking-Ressourcen wie JavaScripts und Stylesheets, die den LCP verzögern könnten.

Leider bestehen die LCP-Metriken trotz all dieser Optimierungen in einigen Fällen immer noch nicht das Core Web Vitals Audit. Warum? Die Größe des Bildes allein reicht aus, um den LCP zu verzögern.

Die Lösung: 2-Stage Loading und Web Worker

Die Lösung, die ich implementiert habe (nachdem ich alle anderen Probleme auf meiner Seite optimiert hatte), ist 2-Stage Image Loading.

Die Idee ist einfach: Zeigen Sie beim ersten Rendern ein Bild mit niedriger Qualität mit genau denselben Abmessungen wie das endgültige Bild mit hoher Qualität an. Unmittelbar nachdem dieses Bild angezeigt wurde, starten Sie den Prozess, der das Bild mit niedriger Qualität gegen ein Bild mit hoher Qualität austauscht.

Eine sehr einfache Implementierung könnte ungefähr so aussehen: Fügen Sie zuerst einen Load Event Listener zu einem Bild hinzu. Wenn das Bild geladen wird, löst sich derselbe Event Listener von selbst und das src des Bildes wird gegen das endgültige Bild mit hoher Qualität ausgetauscht.

<img 
     width="100" 
     height="100" 
     alt="ein Alt-Text" 
     src="lq.webp" 
     onload="this.onload=null;this.src='hq.webp'"
>

Stufe 1: niedrige Qualität webp 3-5kb

Stufe 2: hohe Qualität webp 20-40kb

Dies mag einfach genug erscheinen (und ist es auch), aber das Austauschen einer großen Anzahl von Bildern früh im Rendering-Prozess führt zu zu viel Aktivität im Main Thread und beeinträchtigt andere Core Web Vitals Metriken.

Deshalb habe ich mich entschieden, einen Teil der Arbeit auf einen Web Worker zu verlagern. Ein Web Worker läuft in einem neuen Thread und hat keinen wirklichen Zugriff auf die aktuelle Seite. Die Kommunikation zwischen dem Web Worker und der Seite erfolgt über ein Nachrichtensystem. Der offensichtliche Vorteil ist, dass wir den Main Thread der Seite selbst nicht verwenden, sondern dort Ressourcen freigeben. Der Nachteil ist, dass die Verwendung eines Web Workers etwas umständlich sein kann.

Der Prozess selbst ist nicht so schwierig. Sobald das DomContentLoaded-Event ausgelöst wurde, sammle ich alle Bilder auf der Seite. Wenn ein Bild geladen wurde, tausche ich es sofort aus. Wenn es nicht geladen wurde (weil das Bild lazy load sein könnte), füge ich einen Event Listener hinzu, der das Bild nach dem Lazy Load austauscht.

Das Ergebnis: spektakulär

Der Code für 2-Stage LCP Loading über einen Web Worker

Hier ist der Code, den ich verwende, um meinen LCP durch 2-Stage Loading und einen Web Worker zu beschleunigen. Der Code auf der Hauptseite ruft einen Webworker auf, der die Bilder abruft. Der Webworker übergibt das Ergebnis als Blob an die Hauptseite. Nach Erhalt des Blobs wird das Bild ausgetauscht.

Worker.js

Der Worker hat einen Job. Er hört auf Nachrichten. Eine Nachricht enthält eine Bild-URL und eine eindeutige Bild-ID. Zuerst wandelt er die Bild-URL in die Version mit hoher Qualität um. In meinem Fall durch Ändern von /lq in /resize in der Bild-URL. Der Worker ruft dann das Bild mit hoher Qualität ab, holt es, erstellt einen Blob und gibt dann den Bild-Blob zusammen mit der eindeutigen ID zurück.
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

Das script.js wird als normales Skript auf der aktiven Webseite ausgeführt. Das Skript lädt zuerst den Worker. Dann durchläuft es alle Bilder auf einer Seite. Dies geschieht früh im Rendering-Prozess. Ein Bild könnte bereits geladen sein und könnte nicht. Wenn ein Bild mit niedriger Qualität bereits geladen ist, wird der Austauschprozess sofort aufgerufen. Wenn es noch nicht geladen ist, fügt es einen Listener an das Bild-Lade-Event an, der den Austauschprozess startet, sobald dieses Bild geladen ist..
Wenn ein Bild geladen ist, wird eine eindeutige ID für dieses Bild generiert. Dies ermöglicht es mir, das Bild auf der Seite leicht wiederzufinden (denken Sie daran, der Worker hat keinen Zugriff auf das DOM, daher kann ich den Bild-DOM-Knoten nicht senden).
Die Bild-URL und die eindeutige ID werden dann an den Worker gesendet.
Wenn der Worker das Bild abgerufen hat, wird es als Blob an das Skript zurückgesendet. Das Skript tauscht schließlich die alte Bild-URL gegen die Blob-URL aus, die vom Web Worker erstellt wurde.

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 mit gepreloadetem LCP-Bild

Your dev team is busy.

Delegate the performance architecture to a specialist. I handle the optimization track while your team ships the product.

Discuss Resource Allocation >>

  • Parallel Workflows
  • Specialized Expertise
  • Faster Delivery
Wie ich meinen LCP um 70% gesenkt habe Core Web Vitals Wie ich meinen LCP um 70% gesenkt habe