- Opublikowano
Naprawa Core Web Vitals w Nuxt 3: szybkie wygrane w LCP i CLS
- Autorzy
Problem: żółte i czerwone Core Web Vitals
Core Web Vitals od Google bezpośrednio wpływają na pozycje w wyszukiwarce. Kiedy sprawdziłem PageSpeed Insights dla naszego sklepu, wyniki nie były najlepsze. LCP było ospałe, a CLS powodowało przeskoki układu strony. Klasyczne problemy z wydajnością, ale poprawki okazały się prostsze, niż się spodziewałem.
Oto co było nie tak i jak to naprawiłem.
Poprawka nr 1: preconnect do zewnętrznych originów
Problem
Nasza strona pobiera zasoby z kilku domen:
- CDN z obrazkami (
deante.b-cdn.net) - Widget zgód na cookies (
consent.cookiefirst.com) - Analityka (
www.googletagmanager.com)
Dla każdej nowej domeny przeglądarka musi wykonać:
- DNS lookup
- Połączenie TCP
- Negocjację TLS
To 200-500 ms opóźnienia zanim w ogóle zacznie pobierać zasób.
Rozwiązanie
Dodaj wskazówki preconnect w konfiguracji Nuxta:
// nuxt.config.ts
export default defineNuxtConfig({
app: {
head: {
link: [
{ rel: 'preconnect', href: 'https://your-cdn.net' },
{ rel: 'preconnect', href: 'https://consent.cookiefirst.com' },
{ rel: 'preconnect', href: 'https://www.googletagmanager.com' },
],
},
},
})
Dlaczego to działa: przeglądarka rozpoczyna handshake od razu przy ładowaniu strony, zanim jeszcze wie, że te zasoby będą jej potrzebne. Kiedy przychodzi właściwy request, połączenie jest już zestawione.
Jak znaleźć swoje zewnętrzne domeny
- Otwórz DevTools → zakładka Network
- Załaduj stronę
- Poszukaj requestów do zewnętrznych domen
- Priorytetyzuj domeny serwujące treść widoczną bez scrollowania
Skup się na:
- CDN z obrazkami hero
- Krytycznych skryptach zewnętrznych
- Web fontach
Nie rób preconnecta do wszystkiego — każda wskazówka ma swój koszt. Trzymaj się 3-5 kluczowych originów.
Poprawka nr 2: dodaj width i height do obrazków
Problem
Kiedy obrazki ładują się bez jawnych wymiarów, przeglądarka nie wie, ile miejsca zarezerwować. W miarę ładowania układ strony przeskakuje — i to jest właśnie CLS (Cumulative Layout Shift).
<!-- Bad: No dimensions -->
<img :src="product.image" alt="Product" />
<!-- Also bad: Only width, no height -->
<img :src="product.image" width="300" alt="Product" />
Rozwiązanie
Zawsze podawaj i width, i height:
<CustomNuxtImg
:src="product.mainPhoto?.fullpath"
width="300"
height="300"
class="aspect-square w-full object-contain"
:alt="productName"
/>
Nawet jeśli faktycznym rozmiarem sterujesz CSS-em, przeglądarka używa tych atrybutów do wyliczenia proporcji jeszcze przed załadowaniem obrazka.
Prawdziwe przykłady z naszego kodu
Kafelki produktów:
<CustomNuxtImg
:src="product.mainPhoto?.fullpath"
width="300"
height="300"
class="mx-auto aspect-square w-4/5 object-contain"
:alt="getProductFullNamePrisma(product, locale)"
/>
Baner hero:
<CustomNuxtImg
:src="banner.backgroundImage.fullpath"
width="1380"
height="450"
class="h-[450px] w-full rounded-3xl object-cover"
:alt="null"
:preload="index === 0"
/>
Logo:
<CustomNuxtImg
src="/_MARKETING/NA WWW/logo.png"
width="190"
height="45"
class="h-auto w-32 md:w-52"
:alt="null"
preload
/>
Siatka kategorii:
<CustomNuxtImg
v-if="element.photo"
width="300"
height="128"
loading="lazy"
:src="element.photo.fullpath"
:placeholder="[512, 256, 60]"
class="h-32 w-full rounded object-cover"
:alt="element.name"
/>
Schemat postępowania
- Zmierz faktyczne wymiary obrazka
- Dodaj atrybuty
widthiheight(nawet jeśli rozmiarem steruje CSS) - Użyj
aspect-ratiow CSS, gdy wymiary są proporcjonalne - Połącz z
object-coveralboobject-contain, żeby dobrze skalować
Poprawka nr 3: preload krytycznych obrazków
Problem
Przeglądarka odkrywa obrazki późno — musi sparsować HTML, potem CSS, a potem ustalić, które obrazki są widoczne. Przy obrazkach hero to opóźnienie zabija LCP.
Rozwiązanie
Użyj propsa preload z Nuxt Image dla obrazków widocznych bez scrollowania:
<CustomNuxtImg
:src="banner.backgroundImage.fullpath"
:preload="index === 0" <!-- Only preload the first banner -->
width="1380"
height="450"
/>
To dodaje <link rel="preload"> do head dokumentu, mówiąc przeglądarce, żeby zaczęła pobieranie od razu.
Ważne: preloaduj tylko te obrazki, które na pewno są widoczne przy pierwszym renderze. Nadmiarowy preload marnuje transfer.
Karuzele i slidery
Preloaduj wyłącznie pierwszy slajd:
<template v-for="(item, index) in items">
<CustomNuxtImg :src="item.image" :preload="index === 0" width="800" height="400" />
</template>
Poprawka nr 4: lazy loading obrazków poniżej pierwszego ekranu
Problem
Ładowanie wszystkich obrazków od razu spowalnia ścieżkę krytyczną.
Rozwiązanie
Dodaj loading="lazy" do obrazków poniżej pierwszego ekranu:
<CustomNuxtImg
v-if="element.photo"
width="300"
height="128"
loading="lazy"
:src="element.photo.fullpath"
/>
To mówi przeglądarce, żeby wstrzymała ładowanie, dopóki obrazek nie znajdzie się blisko viewportu.
Czego NIE lazy loadować
- Obrazków hero
- Pierwszego banera w karuzeli
- Logo
- Ikon nawigacji
- Czegokolwiek na pierwszym ekranie
Łączny efekt
Po tych zmianach w 5 plikach:
| Metryka | Przed | Po |
|---|---|---|
| LCP | 4,2 s | 2,1 s |
| CLS | 0,25 | 0,05 |
| Wynik PageSpeed | 58 | 89 |
Najwięcej dało:
- Preconnect do CDN (LCP w dół o około 1 s)
- Wymiary obrazków (CLS blisko zera)
- Preload obrazka hero (LCP w dół o około 0,5 s)
Szybka checklista
Dla LCP (Largest Contentful Paint)
- Dodaj wskazówki preconnect dla CDN i kluczowych domen zewnętrznych
- Preloaduj obrazki hero/banery propsem
preload - Upewnij się, że element LCP jest w początkowym HTML-u (a nie doładowywany JS-em)
- Sprawdź format obrazków (WebP/AVIF zamiast JPEG/PNG)
Dla CLS (Cumulative Layout Shift)
- Dodaj
widthiheightdo WSZYSTKICH obrazków - Ustaw jawne wymiary slotów reklamowych
- Zarezerwuj miejsce na treści dynamiczne
- Unikaj wstawiania treści nad już wyrenderowaną treścią
Dla FCP/TTI (bonus)
- Lazy loading obrazków poniżej pierwszego ekranu
- Placeholder/skeleton pod obrazki
- Odrocz niekrytyczne skrypty zewnętrzne
Ściąga z Nuxt Image
<!-- Hero image: preload, dimensions, no lazy -->
<CustomNuxtImg src="/hero.jpg" width="1920" height="600" preload :placeholder="[1920, 600, 50]" />
<!-- Product grid: lazy, dimensions, placeholder -->
<CustomNuxtImg
:src="product.image"
width="300"
height="300"
loading="lazy"
:placeholder="[300, 300, 30]"
/>
<!-- Logo: preload, small dimensions -->
<CustomNuxtImg src="/logo.svg" width="190" height="45" preload />
Monitorowanie Core Web Vitals
Na developmencie
// plugins/web-vitals.client.ts
import { onCLS, onFCP, onLCP, onTTFB } from 'web-vitals'
export default defineNuxtPlugin(() => {
if (process.dev) {
onCLS(console.log)
onFCP(console.log)
onLCP(console.log)
onTTFB(console.log)
}
})
Na produkcji
- Google Search Console → raport Core Web Vitals
- PageSpeed Insights do sprawdzeń na żądanie
- Rozszerzenie Web Vitals do Chrome dla monitoringu na żywo
Wnioski
Naprawa Core Web Vitals nie musi być skomplikowana:
- Preconnect do CDN i kluczowych zewnętrznych domen
- Wymiary przy każdym obrazku
- Preload obrazków hero
- Lazy loading wszystkiego poniżej pierwszego ekranu
Te cztery zmiany zajęły 30 minut i podniosły nasz wynik PageSpeed z 58 na 89. Efekt SEO jest realny — Google wprost używa Core Web Vitals jako czynnika rankingowego.
Prawdziwe poprawki ze sklepu internetowego. 5 zmienionych plików, wynik PageSpeed wyżej o 31 punktów.