Jak obniżyłem LCP o 70%

Poznaj zaawansowane metody poprawy Core Web Vitals

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

Poprawa metryk LCP za pomocą web workers i dwuetapowego ładowania obrazów

W większości przypadków duży element obrazu w widocznym obszarze ekranu stanie się elementem Largest Contentful Paint. Nawet po zastosowaniu wszystkich najlepszych praktyk Lighthouse, takich jak zmiana rozmiaru obrazów, kompresja obrazów, konwersja do WebP i preloading elementu LCP, Twój Largest Contentful Paint wciąż może nie przejść audytu Core Web Vitals.

Jedynym sposobem na naprawienie tego jest zastosowanie bardziej zaawansowanych technik, takich jak dwuetapowe ładowanie i wątkowanie strony za pomocą web workers, aby zwolnić zasoby na głównym wątku.

W tym artykule pokażę, jak jeszcze bardziej poprawić Largest Contentful Paint.

Dlaczego powinienem preloadować obraz Largest Contentful Paint

Trochę kontekstu

Jestem specjalistą od szybkości stron, a moja strona internetowa jest moją wizytówką. Na stronie głównej z dumą deklaruję, że moja strona jest najszybszą stroną na świecie. Dlatego potrzebuję, aby moja strona ładowała się tak szybko, jak to możliwe i wycisnąć każdą kroplę wydajności z mojej witryny.

Techniki, które Ci dzisiaj pokażę, mogą nie być wykonalne dla przeciętnej strony (WordPress) bez wsparcia dedykowanego i utalentowanego zespołu deweloperów. Jeśli nie możesz powielić tej techniki na swojej stronie, zachęcam Cię do przeczytania artykułu i poznania mojego sposobu myślenia o szybkości stron oraz moich rozważań.

Problem: duże obrazy w widocznym obszarze ekranu

Duży obraz w widocznym obszarze ekranu często staje się elementem Largest Contentful Paint. Często zdarza się, że ten obraz LCP nie przechodzi audytu Core Web Vitals. Takie wyniki widzę na co dzień.

słabe LCP z dużym obrazem

Istnieje kilka sposobów, aby upewnić się, że ten element pojawi się na ekranie szybko:

  1. Preloaduj element LCP. Preloading obrazu LCP zapewni, że ten obraz będzie dostępny dla przeglądarki tak wcześnie, jak to możliwe.
  2. Używaj responsywnych obrazów. Upewnij się, że nie serwujesz obrazów w rozmiarze desktopowym na urządzenia mobilne.
  3. Kompresuj obrazy. Kompresja obrazów może drastycznie zmniejszyć rozmiar obrazu.
  4. Używaj formatów obrazów nowej generacji. Formaty obrazów nowej generacji, takie jak WebP, przewyższają starsze formaty, takie jak JPEG i PNG, w prawie wszystkich przypadkach.
  5. Minimalizuj krytyczną ścieżkę renderowania. Wyeliminuj wszystkie zasoby blokujące renderowanie, takie jak JavaScript i arkusze stylów, które mogą opóźniać LCP.

Niestety, pomimo wszystkich tych optymalizacji, w niektórych przypadkach metryki LCP mogą wciąż nie przechodzić audytu Core Web Vitals. Dlaczego? Sam rozmiar obrazu wystarczy, aby opóźnić LCP.

Rozwiązanie: dwuetapowe ładowanie i web workers

Rozwiązanie, które wdrożyłem (po zoptymalizowaniu wszystkich innych problemów na mojej stronie), to dwuetapowe ładowanie obrazów.

Idea jest prosta: przy pierwszym renderowaniu wyświetl obraz niskiej jakości o dokładnie takich samych wymiarach jak docelowy obraz wysokiej jakości. Natychmiast po wyświetleniu tego obrazu rozpocznij proces zamiany obrazu niskiej jakości na obraz wysokiej jakości.

Bardzo podstawowa implementacja może wyglądać mniej więcej tak: Najpierw dodaj nasłuchiwacz zdarzenia load do obrazu. Gdy obraz się załaduje, ten sam nasłuchiwacz odłącza się, a src obrazu jest zamieniany na finalny obraz wysokiej jakości.

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

Etap 1: obraz webp niskiej jakości 3-5kb two stage image loading lq

Etap 2: obraz webp wysokiej jakości 20-40kb two stage image loading hq

Może się to wydawać wystarczająco proste (i tak jest), ale zamiana dużej liczby obrazów na wczesnym etapie procesu renderowania spowoduje zbyt dużą aktywność na głównym wątku i wpłynie na inne metryki Core Web Vitals.

Dlatego zdecydowałem się przenieść część pracy do web workera. Web worker działa w nowym wątku i nie ma bezpośredniego dostępu do bieżącej strony. Komunikacja między web workerem a stroną odbywa się poprzez system wiadomości. Oczywistą zaletą jest to, że nie używamy głównego wątku strony, zwalniamy tam zasoby. Wadą jest to, że korzystanie z web workera może być nieco uciążliwe.

Sam proces nie jest taki trudny. Po wywołaniu zdarzenia DOMContentLoaded zbieram wszystkie obrazy na stronie. Jeśli obraz został już załadowany, natychmiast go zamieniam. Jeśli nie został jeszcze załadowany (ponieważ obraz może być ładowany leniwie), dołączam nasłuchiwacz zdarzeń, który zamienia obraz po leniwym załadowaniu.

Rezultat: spektakularny

good lcp visible image

Kod do dwuetapowego ładowania LCP przez web worker

Oto kod, którego używam do przyspieszenia LCP poprzez dwuetapowe ładowanie i web worker. Kod na stronie głównej wywołuje web workera, który pobiera obrazy. Web worker przekazuje wynik jako blob do strony głównej. Po otrzymaniu bloba obraz jest zamieniany.

Worker.js

Worker ma jedno zadanie. Nasłuchuje wiadomości. Wiadomość będzie zawierać URL obrazu i unikalny identyfikator obrazu. Najpierw przekształci URL obrazu na wersję wysokiej jakości. W moim przypadku poprzez zmianę /lq na /resize w URL-u obrazu. Następnie worker pobierze obraz wysokiej jakości, utworzy blob i zwróci blob obrazu wraz z unikalnym identyfikatorem.
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

Plik script.js będzie działać jako zwykły skrypt na aktywnej stronie internetowej. Skrypt najpierw ładuje workera. Następnie przechodzi przez wszystkie obrazy na stronie. Dzieje się to na wczesnym etapie procesu renderowania. Obraz może już być załadowany lub nie. Jeśli obraz niskiej jakości jest już załadowany, natychmiast wywoła proces zamiany. Jeśli nie jest jeszcze załadowany, dołączy nasłuchiwacz do zdarzenia load obrazu, który rozpocznie proces zamiany, gdy tylko obraz zostanie załadowany.
Gdy obraz jest załadowany, generowany jest dla niego unikalny identyfikator. Pozwala mi to łatwo odnaleźć obraz na stronie ponownie (pamiętaj, worker nie ma dostępu do DOM, więc nie mogę wysłać węzła DOM obrazu).
URL obrazu i unikalny identyfikator są następnie wysyłane do workera.
Gdy worker pobierze obraz, jest on odsyłany do skryptu jako blob. Skrypt ostatecznie zamienia stary URL obrazu na URL bloba utworzony przez web workera.

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

Wynik Core Web Vitals z preloadowanym obrazem LCP

Make decisions with Data.

You cannot optimize what you do not measure. Install the CoreDash pixel and capture 100% of user experiences.

Create Free Account >>

  • 100% Capture
  • Data Driven
  • Easy Install
Jak obniżyłem LCP o 70%Core Web Vitals Jak obniżyłem LCP o 70%