Jak obniżyłem LCP o 70%
Poznaj zaawansowane metody poprawy Core Web Vitals

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.

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ń.

Istnieje kilka sposobów, aby upewnić się, że ten element pojawi się na ekranie szybko:
- Preloaduj element LCP. Preloading obrazu LCP zapewni, że ten obraz będzie dostępny dla przeglądarki tak wcześnie, jak to możliwe.
- Używaj responsywnych obrazów. Upewnij się, że nie serwujesz obrazów w rozmiarze desktopowym na urządzenia mobilne.
- Kompresuj obrazy. Kompresja obrazów może drastycznie zmniejszyć rozmiar obrazu.
- 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.
- 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 
Etap 2: obraz webp wysokiej jakości 20-40kb 
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

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
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.
- 100% Capture
- Data Driven
- Easy Install

