Wprowadzenie: Dlaczego Core Web Vitals wciąż mają znaczenie w 2026 roku?
Core Web Vitals (CWV) od lat stanowią fundament oceny jakości doświadczeń użytkowników w sieci. Ale w 2026 roku ich znaczenie jest większe niż kiedykolwiek — i szczerze? Wiele osób wciąż tego nie docenia. Google systematycznie zaostrzył kryteria oceny, wprowadził podejście mobile-first z większą wagą dla wyników mobilnych, a przy okazji zaczął uwzględniać nowe metryki, takie jak Engagement Reliability. Strony, które nie spełniają wymagań CWV, tracą pozycje w wyszukiwarce i — co chyba jeszcze ważniejsze — tracą użytkowników.
W tym przewodniku przeprowadzimy Cię krok po kroku przez optymalizację wszystkich trzech głównych metryk: Largest Contentful Paint (LCP), Interaction to Next Paint (INP) oraz Cumulative Layout Shift (CLS). Każdą metrykę omawiamy od podstaw, z konkretnymi technikami i przykładami kodu. Żadnej suchej teorii — same praktyczne rozwiązania.
Trzy filary Core Web Vitals w 2026 roku
Zanim przejdziemy do konkretów, szybkie przypomnienie aktualnych progów:
- LCP (Largest Contentful Paint) — mierzy czas renderowania największego widocznego elementu na stronie. Cel: ≤ 2,5 sekundy.
- INP (Interaction to Next Paint) — mierzy responsywność strony na interakcje użytkownika. Zastąpił FID w marcu 2024. Cel: ≤ 200 milisekund.
- CLS (Cumulative Layout Shift) — mierzy stabilność wizualną, czyli niespodziewane przesunięcia elementów. Cel: ≤ 0,1.
Co się zmieniło? W 2026 roku Google rozszerzył zakres pomiaru LCP — teraz uwzględniane są również obrazy tła ładowane przez CSS oraz dynamicznie wstrzykiwana treść. To oznacza, że strony, które do tej pory spokojnie „przechodziły" testy, mogą teraz potrzebować dodatkowej optymalizacji. Warto to sprawdzić.
Część 1: Optymalizacja LCP — szybkie renderowanie głównej treści
Czym właściwie jest element LCP?
Element LCP to największy widoczny element w oknie przeglądarki (viewport) podczas ładowania strony. Może to być duży obraz hero, blok tekstu, element <video>, a nawet obraz tła w CSS. Chrome automatycznie identyfikuje ten element i mierzy, jak szybko zostaje wyrenderowany.
Cztery fazy LCP
Żeby skutecznie walczyć z wolnym LCP, trzeba zrozumieć z czego się on składa. Każda z czterech faz wpływa na końcowy wynik:
- Time to First Byte (TTFB) — czas od żądania do pierwszego bajtu odpowiedzi serwera
- Resource Load Delay — opóźnienie między TTFB a momentem rozpoczęcia ładowania zasobu LCP
- Resource Load Duration — czas potrzebny na pobranie samego zasobu (np. obrazu)
- Element Render Delay — czas od załadowania zasobu do jego wyrenderowania na ekranie
Technika 1: Redukcja TTFB za pomocą Edge Computing
W 2026 roku Edge SSR (Server-Side Rendering na brzegu sieci) to praktycznie standard dla aplikacji wymagających szybkiego czasu odpowiedzi. Zamiast przetwarzać żądania w jednym centralnym data center, renderowanie odbywa się na serwerach CDN zlokalizowanych blisko użytkownika. Różnica potrafi być ogromna.
// Przykład konfiguracji Edge SSR z Vercel Edge Functions
export const config = {
runtime: 'edge',
regions: ['waw1', 'fra1', 'cdg1'], // Bliskie regiony dla polskich użytkowników
};
export default async function handler(request) {
const html = await renderPage(request.url);
return new Response(html, {
headers: {
'Content-Type': 'text/html; charset=utf-8',
'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=300',
},
});
}
Kluczowe elementy tej strategii:
- Streaming SSR — pozwala przesyłać HTML fragmentami, co może zmniejszyć postrzegany czas ładowania nawet o 40%. Użytkownik widzi treść progresywnie zamiast czekać na pełne renderowanie.
- Stale-While-Revalidate — nagłówek cache, który serwuje cachowaną wersję strony natychmiast, jednocześnie odświeżając ją w tle. Genialne w swojej prostocie.
- Regionalne punkty obecności (PoP) — wybór serwerów CDN w Europie Środkowej minimalizuje opóźnienia sieciowe dla polskich użytkowników.
Technika 2: Preload zasobu LCP
Jednym z najczęstszych problemów z LCP jest zbyt późne „odkrycie" głównego zasobu przez przeglądarkę. Jeśli obraz hero jest wskazywany przez CSS albo ładowany dynamicznie przez JavaScript, przeglądarka po prostu nie wie o nim do momentu parsowania tych zasobów. A to stracony czas.
<!-- Preload obrazu hero — przeglądarka zaczyna go ładować natychmiast -->
<link rel="preload" as="image" href="/images/hero-banner.avif"
type="image/avif"
fetchpriority="high">
<!-- Dla responsywnych obrazów: -->
<link rel="preload" as="image"
imagesrcset="/img/hero-400.avif 400w,
/img/hero-800.avif 800w,
/img/hero-1200.avif 1200w"
imagesizes="100vw"
fetchpriority="high">
Atrybut fetchpriority="high" dodatkowo mówi przeglądarce, że ten zasób ma najwyższy priorytet pobierania. W 2026 roku jest to wspierane przez wszystkie główne przeglądarki, więc nie ma powodu, żeby tego nie używać.
Technika 3: Nowoczesne formaty obrazów
Zastosowanie formatów AVIF i WebP potrafi drastycznie zmniejszyć rozmiar obrazów — przy zachowaniu praktycznie tej samej jakości:
<picture>
<source srcset="/images/hero.avif" type="image/avif">
<source srcset="/images/hero.webp" type="image/webp">
<img src="/images/hero.jpg"
alt="Opis obrazu"
width="1200" height="600"
fetchpriority="high"
decoding="async">
</picture>
Format AVIF oferuje nawet 50% lepszą kompresję w porównaniu z JPEG. W praktyce: obraz hero o rozmiarze 200 KB w JPEG może ważyć zaledwie 80–100 KB jako AVIF. To bezpośrednio przekłada się na szybsze LCP — szczególnie na wolniejszych połączeniach mobilnych.
Technika 4: Eliminacja zasobów blokujących renderowanie
CSS i JavaScript blokujące renderowanie to jedne z najczęstszych przyczyn opóźnień LCP. Dobra wiadomość — da się je dość łatwo wyeliminować:
<!-- Krytyczny CSS osadzony inline -->
<style>
/* Tylko style potrzebne do pierwszego ekranu */
.hero { display: flex; min-height: 60vh; }
.hero img { width: 100%; height: auto; object-fit: cover; }
.nav { display: flex; justify-content: space-between; padding: 1rem; }
</style>
<!-- Reszta CSS ładowana asynchronicznie -->
<link rel="preload" href="/css/main.css" as="style"
onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/css/main.css"></noscript>
<!-- JavaScript z atrybutem defer -->
<script src="/js/app.js" defer></script>
Wyodrębnienie krytycznego CSS i osadzenie go inline w <head> eliminuje jedno żądanie HTTP, które normalnie blokowałoby renderowanie. Narzędzia takie jak Critical (pakiet npm) automatyzują cały ten proces — więc naprawdę nie ma wymówek.
Część 2: Optymalizacja INP — responsywność na interakcje użytkownika
Jak działa INP?
INP mierzy opóźnienie wszystkich interakcji użytkownika na stronie — kliknięć, dotknięć, naciśnięć klawiszy — i raportuje wartość reprezentującą ogólną responsywność. W odróżnieniu od starego FID, który mierzył tylko pierwszą interakcję, INP bierze pod uwagę cały cykl życia strony. To dużo bardziej uczciwa metryka.
Każda interakcja składa się z trzech faz:
- Input Delay — czas od momentu interakcji do rozpoczęcia obsługi zdarzenia (opóźniony przez inne zadania na głównym wątku)
- Processing Time — czas wykonywania handlerów zdarzeń
- Presentation Delay — czas od zakończenia handlera do wyrenderowania następnej klatki
Technika 1: Dzielenie długich zadań za pomocą scheduler.yield()
Długie zadania (Long Tasks) — czyli operacje JavaScript trwające ponad 50 ms — blokują główny wątek i uniemożliwiają przeglądarce reagowanie na interakcje. To chyba najczęstsza przyczyna słabego INP. Na szczęście nowoczesne API scheduler.yield() pozwala oddać kontrolę przeglądarce w trakcie złożonych operacji:
// Bez optymalizacji — jedno długie zadanie blokujące wątek
function processLargeDataset(items) {
for (const item of items) {
heavyComputation(item); // Blokuje główny wątek
updateDOM(item);
}
}
// Z optymalizacją — dzielenie pracy z scheduler.yield()
async function processLargeDataset(items) {
for (const item of items) {
heavyComputation(item);
updateDOM(item);
// Oddaj kontrolę przeglądarce po każdym elemencie
if (navigator.scheduling?.isInputPending()) {
await scheduler.yield();
}
}
}
// Fallback dla przeglądarek bez wsparcia scheduler.yield()
function yieldToMain() {
if ('scheduler' in window && 'yield' in scheduler) {
return scheduler.yield();
}
return new Promise(resolve => setTimeout(resolve, 0));
}
I tu jest najfajniejsza rzecz: kluczowa przewaga scheduler.yield() nad setTimeout() polega na tym, że po oddaniu kontroli, Twoje zadanie trafia na początek kolejki, a nie na jej koniec. Przeglądarka obsłuży oczekujące interakcje użytkownika, a potem natychmiast wróci do Twojego kodu. Bez niepotrzebnych opóźnień.
Technika 2: Diagnostyka z Long Animation Frames API
Long Animation Frames API (LoAF), wprowadzone w Chrome 123, to naprawdę potężne narzędzie diagnostyczne. W przeciwieństwie do starszego Long Tasks API, LoAF dostarcza szczegółowe informacje o skryptach, stylach i layoutach blokujących poszczególne klatki. Wreszcie można dokładnie powiedzieć, co powoduje problemy:
// Monitorowanie długich klatek animacji
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// Klatki dłuższe niż 100ms
if (entry.duration > 100) {
console.warn('Długa klatka animacji:', {
duration: `${entry.duration}ms`,
blockingDuration: `${entry.blockingDuration}ms`,
startTime: entry.startTime,
scripts: entry.scripts.map(s => ({
sourceURL: s.sourceURL,
sourceFunctionName: s.sourceFunctionName,
duration: `${s.duration}ms`,
invokerType: s.invokerType,
})),
});
}
}
});
observer.observe({ type: 'long-animation-frame', buffered: true });
LoAF wskazuje dokładnie który skrypt, z jakiego pliku i jaka funkcja powoduje blokowanie klatki. To nieocenione przy diagnozowaniu problemów wywoływanych przez skrypty zewnętrzne — trackery analityczne, widgety reklamowe, czaty. Wiadomo, te skrypty, o których wszyscy wiemy, ale nikt nie chce ich ruszać.
Technika 3: Optymalizacje specyficzne dla frameworków
Każdy framework frontendowy oferuje własne narzędzia do poprawy responsywności. Zobaczmy, co mamy do dyspozycji w najpopularniejszych:
React — useTransition i useDeferredValue
import { useState, useTransition, useDeferredValue } from 'react';
function SearchResults({ query }) {
const [isPending, startTransition] = useTransition();
const [results, setResults] = useState([]);
const deferredQuery = useDeferredValue(query);
function handleSearch(e) {
const value = e.target.value;
// Aktualizacja inputa — wysoki priorytet (natychmiastowa odpowiedź)
setInputValue(value);
// Wyszukiwanie wyników — niski priorytet (może poczekać)
startTransition(() => {
const filtered = filterResults(value);
setResults(filtered);
});
}
return (
<div>
<input onChange={handleSearch} />
{isPending && <Spinner />}
<ResultsList results={results} />
</div>
);
}
Vue 3 — async components i v-memo
<!-- Lazy-loaded komponent — ładowany tylko gdy potrzebny -->
<script setup>
import { defineAsyncComponent, shallowRef } from 'vue';
const HeavyChart = defineAsyncComponent(() =>
import('./components/HeavyChart.vue')
);
// shallowRef dla dużych kolekcji — unika głębokiej reaktywności
const chartData = shallowRef([]);
</script>
<template>
<!-- v-memo zapobiega zbędnym re-renderom -->
<div v-for="item in items" :key="item.id" v-memo="[item.id, item.updated]">
{{ item.name }}
</div>
<Suspense>
<HeavyChart :data="chartData" />
</Suspense>
</template>
Technika 4: Zarządzanie skryptami zewnętrznymi
No dobra, powiedzmy to wprost — skrypty third-party to jeden z największych „zabójców" INP. Google Analytics, Facebook Pixel, narzędzia do A/B testów, czaty na żywo — każdy z nich dokłada JavaScript na główny wątek. Pytanie nie brzmi „czy wpływają na wydajność", tylko „jak bardzo". Oto strategia zarządzania nimi:
// Strategia Partytown — przeniesienie skryptów third-party do Web Workera
// W nagłówku HTML:
<script>
partytown = {
forward: ['dataLayer.push', 'gtag'],
resolveUrl: (url) => {
// Proxy dla skryptów blokowanych przez CORS
if (url.hostname !== location.hostname) {
return new URL(`/proxy?url=${encodeURIComponent(url)}`, location.origin);
}
return url;
}
};
</script>
<script src="/~partytown/partytown.js"></script>
<!-- Skrypt GA4 uruchamiany w Web Workerze -->
<script type="text/partytown"
src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX">
</script>
Przeniesienie skryptów analitycznych i reklamowych do Web Workera za pomocą Partytown może zmniejszyć blokowanie głównego wątku nawet o 90%. Tak, dziewięćdziesiąt procent. To bezpośrednio przekłada się na lepszy INP.
Część 3: Optymalizacja CLS — stabilność wizualna strony
Dlaczego CLS jest „najczęściej niezrozumianą" metryką?
CLS mierzy niespodziewane przesunięcia elementów w całym czasie życia strony — nie tylko podczas ładowania. I tu wiele osób popełnia błąd, skupiając się wyłącznie na fazie wczytywania.
W 2026 roku Google stosuje metodę sesji okien: grupuje przesunięcia w okna czasowe o maksymalnej długości 5 sekund (z przerwami nie dłuższymi niż 1 sekunda) i wybiera okno z największą sumą przesunięć. Oznacza to, że nawet pojedyncze duże przesunięcie po załadowaniu strony — na przykład wyskakujący banner cookie po 3 sekundach — może zdominować wynik CLS.
Technika 1: Rezerwacja miejsca dla dynamicznej treści
Najczęstsza przyczyna złego CLS? Brak zarezerwowanego miejsca dla elementów ładowanych asynchronicznie. To naprawdę proste do naprawienia, a mimo to wiele stron wciąż tego nie robi:
/* Kontener na reklamę z zarezerwowaną przestrzenią */
.ad-container {
min-height: 250px; /* Stała minimalna wysokość */
aspect-ratio: 300 / 250; /* Proporcje reklamy */
contain: layout; /* CSS Containment — izoluje layout */
content-visibility: auto; /* Optymalizacja renderowania */
background: #f0f0f0; /* Placeholder wizualny */
}
/* Skeleton loader dla karty produktu */
.product-card-skeleton {
width: 100%;
height: 380px; /* Taka sama jak docelowa karta */
border-radius: 8px;
background: linear-gradient(
90deg,
#f0f0f0 25%,
#e0e0e0 50%,
#f0f0f0 75%
);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
Ważna uwaga: skeleton loadery muszą odpowiadać wysokości docelowej treści. Jeśli skeleton ma 200 px, a docelowa karta produktu 380 px — przesunięcie i tak nastąpi. Warto używać min-height opartego na realnych danych z produkcji.
Technika 2: Wymiary obrazów i atrybuty width/height
To brzmi banalnie, ale zawsze określaj wymiary obrazów. Zawsze. W atrybutach HTML lub za pomocą CSS aspect-ratio:
<!-- Najlepsza praktyka — jawne wymiary -->
<img src="/products/shoe.webp"
alt="Buty sportowe"
width="600"
height="400"
loading="lazy"
decoding="async">
<!-- Alternatywnie — CSS aspect-ratio dla responsywnych obrazów -->
<style>
.responsive-img {
width: 100%;
height: auto;
aspect-ratio: 3 / 2; /* Przeglądarka rezerwuje miejsce */
object-fit: cover;
}
</style>
<!-- Dla elementów iframe (np. YouTube) -->
<div style="aspect-ratio: 16/9; width: 100%;">
<iframe src="https://www.youtube.com/embed/xyz"
width="100%" height="100%"
loading="lazy"
title="Tytuł wideo">
</iframe>
</div>
Technika 3: Optymalizacja webfontów
Czcionki internetowe to (jak to lubię mówić) „cichy zabójca" CLS. Efekt FOUT (Flash of Unstyled Text) powoduje przesunięcia, gdy systemowa czcionka zastępcza zostaje zamieniona na docelową czcionkę webową o innych metrykach. Różnica w wysokości liter czy odstępach powoduje, że cały layout się przesuwa:
<!-- 1. Preload najważniejszych czcionek -->
<link rel="preload" href="/fonts/inter-var.woff2"
as="font" type="font/woff2" crossorigin>
<!-- 2. Dopasowanie metryk czcionki fallbackowej za pomocą CSS -->
<style>
/* Czcionka override — dopasowuje systemową czcionkę do docelowej */
@font-face {
font-family: 'Inter Fallback';
src: local('Arial');
ascent-override: 90.49%;
descent-override: 22.56%;
line-gap-override: 0%;
size-adjust: 107.06%;
}
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-var.woff2') format('woff2');
font-display: swap;
font-weight: 100 900;
}
body {
font-family: 'Inter', 'Inter Fallback', sans-serif;
}
</style>
Technika font metric override dostosowuje metryki czcionki fallbackowej (np. Arial) do metryk docelowej czcionki webowej. Efekt? Zamiana czcionek nie powoduje żadnych przesunięć layoutu — tekst zajmuje dokładnie tyle samo miejsca przed i po załadowaniu czcionki. Proste, a robi ogromną różnicę.
Technika 4: CSS Containment i content-visibility
Właściwości contain i content-visibility pozwalają izolować elementy od reszty layoutu i optymalizować renderowanie elementów poza viewport:
/* Izolacja layoutu — zmiany wewnątrz nie wpływają na resztę strony */
.widget-container {
contain: layout style;
}
/* Automatyczne pomijanie renderowania elementów poza widokiem */
.article-section {
content-visibility: auto;
contain-intrinsic-size: auto 500px; /* Szacunkowa wysokość sekcji */
}
/* Zapobieganie przesunięciom przy animacjach */
.animated-element {
/* Używaj transform zamiast top/left/margin */
transform: translateY(0);
transition: transform 0.3s ease;
will-change: transform;
}
.animated-element.active {
transform: translateY(-10px); /* Nie powoduje layout shift */
}
content-visibility: auto potrafi drastycznie przyspieszyć renderowanie długich stron — list produktów, artykułów, katalogów. Przeglądarka po prostu pomija renderowanie sekcji, które nie są widoczne w viewport. Jednocześnie contain-intrinsic-size zapobiega przesunięciom layoutu, podając przybliżony rozmiar elementu. Win-win.
Część 4: Web Workers — przeniesienie ciężkich obliczeń poza główny wątek
Kiedy sięgać po Web Workerów?
Web Workers pozwalają uruchomić JavaScript w oddzielnym wątku, całkowicie niezależnym od głównego wątku UI. To idealne rozwiązanie dla operacji, które blokują główny wątek i pogarszają INP — parsowanie dużych zestawów danych, skomplikowana walidacja, kryptografia, przetwarzanie obrazów po stronie klienta.
Prosta reguła: jeśli operacja trwa ponad 50 ms i nie wymaga bezpośredniego dostępu do DOM — przenieś ją do Web Workera. Worker nie ma dostępu do document ani window, ale może wykonywać obliczenia i przesyłać wyniki z powrotem za pomocą postMessage().
// worker.js — przetwarzanie danych w osobnym wątku
self.addEventListener('message', (event) => {
const { type, payload } = event.data;
switch (type) {
case 'FILTER_PRODUCTS': {
// Filtrowanie i sortowanie 10 000+ produktów
const filtered = payload.products
.filter(p => p.price >= payload.minPrice && p.price <= payload.maxPrice)
.filter(p => payload.categories.includes(p.category))
.sort((a, b) => {
if (payload.sortBy === 'price') return a.price - b.price;
if (payload.sortBy === 'rating') return b.rating - a.rating;
return a.name.localeCompare(b.name);
});
self.postMessage({ type: 'FILTER_RESULT', payload: filtered });
break;
}
case 'PARSE_CSV': {
// Parsowanie dużego pliku CSV
const rows = payload.csv.split('\n').map(row => row.split(','));
const headers = rows[0];
const data = rows.slice(1).map(row =>
Object.fromEntries(headers.map((h, i) => [h.trim(), row[i]?.trim()]))
);
self.postMessage({ type: 'CSV_PARSED', payload: data });
break;
}
}
});
// main.js — wykorzystanie Web Workera z głównego wątku
const worker = new Worker('/worker.js');
// Słuchanie odpowiedzi z workera
worker.addEventListener('message', (event) => {
const { type, payload } = event.data;
if (type === 'FILTER_RESULT') {
// Natychmiastowa aktualizacja UI z przefiltrowanymi danymi
renderProducts(payload);
}
});
// Wysyłanie zadania do workera — nie blokuje głównego wątku
function filterProducts(filters) {
// Pokaż spinner natychmiast (główny wątek jest wolny!)
showLoadingIndicator();
worker.postMessage({
type: 'FILTER_PRODUCTS',
payload: {
products: allProducts,
...filters
}
});
}
Comlink — uproszczona komunikacja z Web Workerami
Ręczne zarządzanie postMessage potrafi być uciążliwe przy większych projektach. Biblioteka Comlink od Google eliminuje cały ten boilerplate, pozwalając wywoływać funkcje w workerze tak, jakby były zwykłymi asynchronicznymi funkcjami:
// heavy-computation.worker.js
import * as Comlink from 'comlink';
const api = {
async processImage(imageData, quality) {
// Ciężkie przetwarzanie obrazu — całkowicie poza głównym wątkiem
const resized = resizeImage(imageData, 800, 600);
const compressed = compressImage(resized, quality);
return compressed;
},
async calculateAnalytics(events) {
// Analiza 100 000+ zdarzeń
return {
totalEvents: events.length,
uniqueUsers: new Set(events.map(e => e.userId)).size,
avgSessionDuration: calculateAvgDuration(events),
topPages: getTopPages(events, 10),
};
}
};
Comlink.expose(api);
// main.js — wywołanie funkcji workera jak zwykłej funkcji async
import * as Comlink from 'comlink';
const worker = new Worker(
new URL('./heavy-computation.worker.js', import.meta.url),
{ type: 'module' }
);
const api = Comlink.wrap(worker);
// Użycie — wygląda jak zwykłe wywołanie async!
async function handleImageUpload(file) {
const imageData = await file.arrayBuffer();
// Przetwarzanie w workerze — główny wątek pozostaje wolny
const processed = await api.processImage(imageData, 0.85);
// Aktualizacja UI z wynikiem
displayImage(processed);
}
Pod spodem Comlink wykorzystuje Proxy i postMessage do transparentnego przesyłania danych między wątkami. Ale dla programisty wygląda to po prostu jak zwykłe wywołanie funkcji. Zdecydowanie warte wypróbowania.
Część 5: Zaawansowane strategie optymalizacji
Speculation Rules API — natychmiastowa nawigacja
Speculation Rules API to nowoczesne podejście do prerenderingu stron, wspierane w 2026 roku przez Chrome i Edge. Pozwala „spekulatywnie" ładować strony, na które użytkownik prawdopodobnie przejdzie. Efekt? Niemal natychmiastowa nawigacja — użytkownik klika link i strona pojawia się od razu:
<script type="speculationrules">
{
"prerender": [
{
"where": {
"and": [
{ "href_matches": "/*" },
{ "not": { "href_matches": "/logout" } },
{ "not": { "href_matches": "/api/*" } },
{ "not": { "selector_matches": "[data-no-prerender]" } }
]
},
"eagerness": "moderate",
"referrer_policy": "no-referrer-when-downgrade"
}
],
"prefetch": [
{
"where": { "href_matches": "/products/*" },
"eagerness": "conservative"
}
]
}
</script>
Poziomy „eagerness" kontrolują, jak agresywnie przeglądarka prerenderuje strony:
- immediate — prerender od razu po wczytaniu strony
- eager — prerender po najechaniu kursorem
- moderate — prerender po najechaniu kursorem z krótkim opóźnieniem (dobry kompromis)
- conservative — prerender dopiero po kliknięciu (na mobile: po dotknięciu)
Optymalizacja bundla JavaScript
W 2026 roku nowoczesne bundlery — Vite, Turbopack, Rspack — oferują zaawansowane techniki code splitting i tree shaking. Oto kilka sprawdzonych praktyk:
// vite.config.js — zaawansowana konfiguracja code splitting
import { defineConfig } from 'vite';
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
// Biblioteki rzadko aktualizowane — osobny chunk z długim cachem
'vendor-react': ['react', 'react-dom', 'react-router-dom'],
'vendor-utils': ['lodash-es', 'date-fns'],
// Kod specyficzny dla tras
'route-home': ['./src/pages/Home.tsx'],
'route-product': ['./src/pages/Product.tsx'],
},
},
},
// Chunk powyżej 150 KB powinien być rozbity
chunkSizeWarningLimit: 150,
reportCompressedSize: true,
},
});
// Dynamiczny import z React.lazy() — ładuj komponenty na żądanie
import { lazy, Suspense } from 'react';
const ProductDetail = lazy(() =>
import(/* webpackChunkName: "product-detail" */ './ProductDetail')
);
const CheckoutFlow = lazy(() =>
import(/* webpackChunkName: "checkout" */ './CheckoutFlow')
);
function App() {
return (
<Suspense fallback={<LoadingSkeleton />}>
<Routes>
<Route path="/product/:id" element={<ProductDetail />} />
<Route path="/checkout" element={<CheckoutFlow />} />
</Routes>
</Suspense>
);
}
Strategia cachowania na poziomie HTTP
Dobrze zaplanowane cachowanie potrafi wyeliminować konieczność ponownego ładowania zasobów. To może wydawać się oczywiste, ale zdziwilibyście się, ile stron ma to źle skonfigurowane:
# Nginx — strategia cachowania dla zasobów statycznych
server {
# Pliki z hashem w nazwie (np. app.a1b2c3.js) — cache na rok
location ~* \.(js|css)$ {
if ($uri ~* "\.[a-f0-9]{8,}\.") {
add_header Cache-Control "public, max-age=31536000, immutable";
}
}
# Obrazy — cache na 30 dni z rewalidacją
location ~* \.(avif|webp|jpg|png|svg)$ {
add_header Cache-Control "public, max-age=2592000, stale-while-revalidate=86400";
}
# Czcionki — cache na rok (rzadko się zmieniają)
location ~* \.(woff2|woff)$ {
add_header Cache-Control "public, max-age=31536000, immutable";
add_header Access-Control-Allow-Origin "*";
}
# HTML — krótki cache z rewalidacją
location ~* \.html$ {
add_header Cache-Control "public, max-age=60, stale-while-revalidate=300";
}
}
Część 6: Pomiary i monitoring w produkcji
Biblioteka web-vitals
Najskuteczniejszym sposobem monitorowania Core Web Vitals jest zbieranie danych od rzeczywistych użytkowników (RUM — Real User Monitoring). Biblioteka web-vitals od Google to absolutny standard branżowy i punkt wyjścia dla każdego poważnego monitoringu:
// Instalacja: npm install web-vitals
import { onLCP, onINP, onCLS } from 'web-vitals';
function sendToAnalytics(metric) {
const data = {
name: metric.name,
value: metric.value,
rating: metric.rating, // 'good', 'needs-improvement', 'poor'
delta: metric.delta,
id: metric.id,
navigationType: metric.navigationType,
// Atrybucja — wskazuje przyczynę problemu
attribution: metric.attribution,
};
// Wysyłanie do systemu analitycznego
navigator.sendBeacon('/api/vitals', JSON.stringify(data));
}
// Monitorowanie z pełną atrybucją
onLCP(sendToAnalytics, { reportAllChanges: false });
onINP(sendToAnalytics, { reportAllChanges: false });
onCLS(sendToAnalytics, { reportAllChanges: false });
Atrybucja w web-vitals wskazuje dokładnie co spowodowało dany wynik. Dla LCP — który element był elementem LCP i ile trwała każda z czterech faz. Dla INP — konkretną interakcję i skrypt. Dla CLS — element, który się przesunął. Bez atrybucji optymalizacja to w dużej mierze strzelanie na oślep.
Narzędzia do monitorowania CWV w 2026
Kompleksowy monitoring wymaga kombinacji narzędzi laboratoryjnych i polowych. Oto moje rekomendacje:
- Chrome DevTools → Performance panel — szczegółowa analiza poszczególnych interakcji, Long Animation Frames, warstw renderowania
- Lighthouse — audyt laboratoryjny z rekomendacjami (ale pamiętaj: wyniki Lighthouse to nie to samo co wyniki polowe!)
- PageSpeed Insights — dane CrUX z rzeczywistych użytkowników Chrome
- Web Vitals Extension — wtyczka Chrome pokazująca CWV w czasie rzeczywistym, świetna do szybkich testów
- DebugBear / SpeedCurve — komercyjne platformy do ciągłego monitorowania z alertami
- Google Search Console — raport Core Web Vitals z danymi wpływającymi na ranking
Automatyzacja w pipeline CI/CD
Integracja testów wydajnościowych w pipeline CI/CD to coś, czego nie da się przecenić. Zapobiega regresji — bo nic tak nie boli, jak wdrożenie zmiany, która psuje wydajność i odkrycie tego dopiero tydzień później:
# .github/workflows/lighthouse-ci.yml
name: Lighthouse CI
on: [pull_request]
jobs:
lighthouse:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
- name: Build
run: npm ci && npm run build
- name: Run Lighthouse CI
uses: treosh/lighthouse-ci-action@v12
with:
configPath: './lighthouserc.json'
uploadArtifacts: true
- name: Assert budgets
run: |
npx lighthouse-ci assert \
--preset=lighthouse:recommended \
--assert.largest-contentful-paint=2500 \
--assert.interaction-to-next-paint=200 \
--assert.cumulative-layout-shift=0.1
Część 7: Checklist — praktyczna lista kontrolna
Na koniec — kompaktowa lista kontrolna, którą możesz zastosować na swojej stronie. Wydrukuj ją, przypnij do monitora, cokolwiek działa. Ważne, żeby ją mieć pod ręką.
LCP — Checklist
- TTFB poniżej 800 ms (idealnie poniżej 200 ms)
- Element LCP ma
fetchpriority="high" - Obraz LCP jest preloadowany za pomocą
<link rel="preload"> - Używasz formatów AVIF/WebP z fallbackiem
- Krytyczny CSS jest osadzony inline
- JavaScript nie blokuje renderowania (defer/async)
- CDN skonfigurowany z punktami obecności blisko użytkowników
- Nagłówki cache'owania prawidłowo ustawione
INP — Checklist
- Brak długich zadań (Long Tasks) powyżej 50 ms na głównym wątku
- Używasz
scheduler.yield()lubsetTimeoutdo dzielenia pracy - Skrypty third-party przeniesione do Web Workera lub odroczone
- Frameworkowe optymalizacje zastosowane (useTransition, v-memo, OnPush)
- Event handlery są lekkie i szybkie
- Ciężkie obliczenia przeniesione do Web Workerów
- Monitorowanie LoAF w produkcji
CLS — Checklist
- Wszystkie obrazy mają atrybuty width i height
- Zarezerwowane miejsce dla reklam, embedów i dynamicznej treści
- Czcionki webowe preloadowane z font metric override
- Animacje używają
transformzamiast właściwości layoutowych - CSS Containment zastosowany dla widgetów i komponentów
- Brak dynamicznie wstrzykiwanej treści powyżej istniejącego contentu
content-visibility: autona długich listach i sekcjach
Wpływ Core Web Vitals na wyniki biznesowe
Optymalizacja Core Web Vitals to nie tylko zabawa techniczna — ma bezpośredni wpływ na pieniądze. Dane z raportów branżowych konsekwentnie to potwierdzają:
- Współczynnik konwersji — każde 100 ms poprawy czasu ładowania może zwiększyć konwersję o 1–2%. Dla sklepu e-commerce z obrotem 10 mln PLN rocznie to potencjalnie 100–200 tys. PLN dodatkowego przychodu. Nie jest źle jak na „techniczny" temat, prawda?
- Współczynnik odrzuceń — strony z LCP powyżej 4 sekund mają nawet 3-krotnie wyższy bounce rate niż te z LCP poniżej 2 sekund. Na mobile jest jeszcze gorzej.
- SEO i widoczność organiczna — Google potwierdził, że Core Web Vitals są sygnałem rankingowym. W konkurencyjnych branżach, gdzie inne czynniki są zbliżone, to CWV mogą przesądzić o pozycji.
- Zaangażowanie użytkowników — szybsze strony generują więcej odsłon na sesję, dłuższy czas na stronie i więcej interakcji. Badania wskazują, że poprawienie INP poniżej 200 ms zwiększa zaangażowanie średnio o 15%.
Warto pamiętać, że Google do rankingu wykorzystuje dane z Chrome User Experience Report (CrUX) — czyli rzeczywiste doświadczenia użytkowników Chrome, nie wyniki syntetycznych testów. Dlatego monitoring RUM jest kluczowy. Możesz mieć idealne wyniki w Lighthouse, ale słabe w CrUX, jeśli Twoi realni użytkownicy korzystają ze słabszych urządzeń lub wolniejszych połączeń. Widziałem to nieraz i to potrafi być frustrujące.
Podsumowanie
Optymalizacja Core Web Vitals w 2026 roku to nie jednorazowe zadanie — to ciągły proces wymagający monitoringu, analizy i iteracji. Oto najważniejsze wnioski:
- LCP wymaga holistycznego podejścia — od Edge SSR i cachowania, przez preload zasobów, po nowoczesne formaty obrazów.
- INP ujawnia problemy z responsywnością w całym cyklu życia strony.
scheduler.yield()i Long Animation Frames API to Twoje najlepsze narzędzia. - CLS jest często niedoceniany, ale wymaga systematycznej pracy — od rezerwacji miejsca przez font metric override po CSS Containment.
- Monitoring RUM jest absolutnie konieczny — wyniki laboratoryjne nie odzwierciedlają doświadczeń rzeczywistych użytkowników.
- Automatyzacja w CI/CD chroni przed regresją i daje spokój ducha przy każdym wdrożeniu.
Zacznij od zmierzenia obecnych wyników za pomocą PageSpeed Insights i biblioteki web-vitals. Zidentyfikuj najsłabsze metryki i systematycznie pracuj nad ich poprawą, korzystając z technik opisanych powyżej. Każda milisekunda ma znaczenie — zarówno dla Twoich użytkowników, jak i dla pozycji w Google.