Klient pisze do Ciebie na Discordzie. "Strona ładuje się wieki, a przyciski nie reagują". Otwierasz PageSpeed Insights. Czerwone pole po czerwonym polu. LCP 4.2 sekundy. INP 780 ms. CLS 0.35. Wygląda jak scena z horroru.
Wiesz co robisz? Klikasz "Zamknij" i udajesz, że tego nie widziałeś. Albo wrzucasz na stronę kolejny skrypt analytics, bo "klient chce mieć dane".
W 2026 roku Google nie żartuje z Core Web Vitals. Od marca 2024 INP (Interaction to Next Paint) oficjalnie zastąpił FID jako jedna z trzech głównych metryk. Strony z czerwonym INP tracą pozycje. Nie teoretycznie. Naprawdę. Widziałem jak strona po optymalizacji CWV wskoczyła z pozycji 12 na 4 w ciągu trzech tygodni.
Dobra wiadomość? Większość problemów z Core Web Vitals da się naprawić w 30 minut. Bez przepisywania całej aplikacji. Bez zmiany hostingu. Bez magii.
W tym artykule pokażę Ci dokładnie jak to zrobić. Krok po kroku. Z kodem. Z komendami. Bez lania wody.
Co się zmieniło w Core Web Vitals w 2024-2026
INP zamiast FID. I to duża zmiana.
FID (First Input Delay) mierzył opóźnienie pierwszej interakcji. Tylko pierwszej. I tylko opóźnienie. Jeśli strona zareagowała po 100 ms na pierwsze kliknięcie, FID był zielony. A potem wszystko się zawieszało? FID tego nie widział.
INP (Interaction to Next Paint) patrzy na wszystkie interakcje na stronie i wybiera najgorszą. Nie tylko pierwszą. Nie tylko opóźnienie. INP mierzy czas od kliknięcia do momentu, kiedy przeglądarka faktycznie coś narysuje na ekranie. To znacznie bardziej realistyczna metryka.
Nowe progi INP w 2026:
- Dobry: do 200 ms
- Do poprawy: 200-500 ms
- Słaby: powyżej 500 ms
FID miał progi 100/300 ms. INP jest bardziej wymagający, ale też bardziej uczciwy.
LCP i CLS bez zmian, ale z nowymi wymaganiami
LCP (Largest Contentful Paint) nadal mierzy czas do wyrenderowania największego elementu widocznego. Progi bez zmian: do 2.5 s jest zielono.
CLS (Cumulative Layout Shift) nadal mierzy przesuwanie się elementów podczas ładowania. Progi: do 0.1 jest zielono.
Ale tu jest haczyk. Google coraz częściej patrzy na CrUX (Chrome User Experience Report) jako źródło prawdy. To dane rzeczywistych użytkowników Chrome, nie syntetyczne testy. Twoja strona może mieć 99 w Lighthouse, a w CrUX czerwone pole. Bo Lighthouse testuje idealne warunki, a użytkownicy mają stare telefony i słabe 3G.
Co to dla Ciebie oznacza:
Optymalizuj pod rzeczywistych użytkowników, nie pod syntetyczny wynik Lighthouse. CrUX to prawda. Lighthouse to symulacja.
LCP: Znajdź winowajcę w 10 sekund
Co to jest LCP i dlaczego strasznie dużo stron to psuje
LCP to czas, w którym największy element widoczny w viewportcie zostaje wyrenderowany. Najczęściej to:
- Obrazek hero na stronie głównej
- Duży baner w sklepie
- Tło w sekcji hero
Problem polega na tym, że większość stron ładuje LCP-obrazek jakby był na końcu kolejki. Przeglądarka najpierw ładuje cały CSS, potem JS, potem fonty, a na sam koniec obrazek. Użytkownik widzi białą stronę przez 4 sekundy, a potem nagle wszystko się pojawia.
Jak znaleźć element LCP w DevTools
Otwórz Chrome DevTools (F12). Przejdź do zakładki Performance. Kliknij nagranie (kółko) i odśwież stronę (Ctrl+R). Zatrzymaj nagranie.
W górnym pasku znajdź zieloną linię "LCP". Najedź na nią. DevTools pokaże Ci dokładnie który element jest Twoim LCP. Zazwyczaj to obrazek lub blok tekstu.
Szybsza metoda: Otwórz Console i wpisz:
// Znajdź aktualny LCP element
new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
console.log('LCP element:', lastEntry.element);
console.log('LCP czas:', lastEntry.startTime);
}).observe({ entryTypes: ['largest-contentful-paint'] });Jak naprawić LCP w 5 minut
Krok 1: Priorytet ładowania obrazka
Jeśli Twój LCP to obrazek, dodaj mu fetchpriority="high". To najważniejsza zmiana jaką możesz zrobić.
<!-- PRZED -->
<img src="hero.jpg" alt="Hero" />
<!-- PO -->
<img src="hero.jpg" alt="Hero" fetchpriority="high" />To mówi przeglądarce: "Ładuj ten obrazek NAJPierw. Nie na końcu. Nie po fontach. Teraz."
Krok 2: Preload LCP jeśli jest w CSS
Jeśli Twój LCP to obrazek tła z CSS (background-image), przeglądarka musi najpierw pobrać i sparsować CSS, a dopiero potem zobaczyć że potrzebuje obrazka. To dodaje 200-500 ms.
Dodaj preload w <head>:
<link rel="preload" as="image" href="/hero.jpg" fetchpriority="high" />Krok 3: Nie używaj background-image dla LCP
background-image w CSS ma niższy priorytet niż <img>. Jeśli to możliwe, zmień LCP-obrazek na <img> z object-fit: cover.
<!-- ZAMIAST background-image w CSS -->
<img
src="hero.jpg"
alt="Hero"
fetchpriority="high"
style="position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;"
/>Krok 4: Optymalizuj format i rozmiar
WebP zawsze. AVIF jeśli możesz. Ale nie rób 4000px obrazka na stronę mobilną.
<!-- Responsywny obrazek -->
<picture>
<source
srcset="hero-400.avif 400w, hero-800.avif 800w, hero-1200.avif 1200w"
type="image/avif"
/>
<source
srcset="hero-400.webp 400w, hero-800.webp 800w, hero-1200.webp 1200w"
type="image/webp"
/>
<img
src="hero-800.jpg"
alt="Hero"
fetchpriority="high"
width="1200"
height="600"
decoding="async"
/>
</picture>Krok 5: Width i height na obrazkach
Zawsze podawaj width i height na <img>. Bez tego przeglądarka nie zarezerwuje miejsca i dostaniesz Layout Shift. A to zabije Twój CLS.
INP: Nowy król wymagający
Dlaczego INP jest taki brutalny
FID mierzył tylko opóźnienie. Kliknąłeś przycisk, przeglądarka zaczęła coś robić po 50 ms, FID był zielony. A potem przeglądarka wykonywała jakiś potężny kod przez 3 sekundy i ekran był zamrożony? FID tego nie widział.
INP patrzy na całość. Od kliknięcia do momentu, kiedy użytkownik widzi rezultat. Jeśli klikniesz "Dodaj do koszyka" i przycisk zmienia się dopiero po 2 sekundach, INP wynosi 2000 ms. Czerwone pole.
Najczęstsze przyczyny słabego INP:
- Długie zadania JavaScript blokujące główny wątek
- setTimeout/setInterval wykonujący ciężkie operacje
- Duże komponenty React renderujące się synchronicznie
- Nieoptymalne event handlery
Jak znaleźć winowajcę INP w DevTools
Otwórz DevTools. Przejdź do zakładki Performance Insights (lub Performance w starszych wersjach).
Nagraj interakcję z stroną. Kliknij w przycisk, który jest wolny. Zatrzymaj nagranie.
W sekcji Interactions zobaczysz każdą interakcję z czasem. Kliknij w interakcję z czerwonym paskiem. DevTools pokaże Ci dokładnie co się działo: skrypt, funkcja, plik.
Alternatywna metoda: web-vitals library
import { onINP } from 'web-vitals';
onINP((metric) => {
console.log('INP:', metric.value, 'ms');
// Jeśli INP > 200 ms, zaloguj szczegóły
if (metric.value > 200) {
metric.entries.forEach((entry) => {
console.log('Interakcja:', entry.name);
console.log('Czas przetwarzania:', entry.processingEnd - entry.processingStart);
console.log('Czas prezentacji:', entry.duration);
});
}
});Jak naprawić INP w 10 minut
Krok 1: Podziel długie zadania JavaScript
Jeśli masz funkcję, która wykonuje się 500 ms, przeglądarka nie może odpowiadać na interakcje przez ten czas. Użyj scheduler.yield() lub podziel zadanie.
// ZŁO - blokuje wątek na 500 ms
function processItems(items) {
for (const item of items) {
heavyCalculation(item);
}
}
// DOBRZE - pozwala przeglądarce na przerwy
async function processItems(items) {
for (const item of items) {
heavyCalculation(item);
// Co 50 elementów oddaj kontrolę przeglądarce
if (items.indexOf(item) % 50 === 0) {
await new Promise((resolve) => setTimeout(resolve, 0));
}
}
}Krok 2: Użyj scheduler.yield() (Chrome 115+)
async function processLargeDataset(data) {
const chunkSize = 100;
for (let i = 0; i < data.length; i += chunkSize) {
const chunk = data.slice(i, i + chunkSize);
processChunk(chunk);
// Oddaj kontrolę głównemu wątkowi
if (scheduler?.yield) {
await scheduler.yield();
} else {
await new Promise((resolve) => setTimeout(resolve, 0));
}
}
}Krok 3: Debounce i throttle na eventach
Scroll i resize eventy odpalają się setki razy na sekundę. Jeśli w handlerze robisz coś ciężkiego, INP umiera.
// ZŁO - wykonuje się 60 razy na sekundę podczas scrollowania
window.addEventListener('scroll', () => {
updateComplexLayout();
});
// DOBRZE - throttle do 60ms (max 16 razy na sekundę)
function throttle(fn, limit) {
let inThrottle;
return function(...args) {
if (!inThrottle) {
fn.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
window.addEventListener('scroll', throttle(() => {
updateComplexLayout();
}, 60));Krok 4: Unikaj layout thrashing
Jeśli czytasz właściwość DOM, potem zapisujesz, potem znowu czytasz... przeglądarka musi przeliczyć layout po każdej operacji.
// ZŁO - layout thrashing
const items = document.querySelectorAll('.item');
items.forEach((item) => {
const height = item.offsetHeight; // READ (przelicza layout)
item.style.height = height * 2 + 'px'; // WRITE (przelicza layout)
});
// DOBRZE - read first, write later
const items = document.querySelectorAll('.item');
const heights = Array.from(items).map((item) => item.offsetHeight); // READ ALL
items.forEach((item, index) => {
item.style.height = heights[index] * 2 + 'px'; // WRITE ALL
});Krok 5: React: użyj useTransition dla ni priorytetowych aktualizacji
import { useTransition, useState } from 'react';
function SearchResults() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();
const handleSearch = (value) => {
setQuery(value); // Wysoka priorytet - natychmiast
startTransition(() => {
setResults(performHeavySearch(value)); // Niska priorytet - nie blokuje UI
});
};
return (
<>
<input
value={query}
onChange={(e) => handleSearch(e.target.value)}
/>
{isPending && <span>Przetwarzam...</span>}
<Results data={results} />
</>
);
}CLS: Skaczące elementy to najgorsze doświadczenie użytkownika
Co to jest CLS i dlaczego ludzie tego nienawidzą
Wyobraź sobie: czytasz artykuł. Nagle obrazek się załaduje i cały tekst przeskakuje w dół o 300 pikseli. Klikasz w przycisk, który był tam sekundę temu, a w jego miejscu jest teraz reklama.
To CLS. Cumulative Layout Shift. Suma wszystkich przesunięć elementów na stronie.
Co generuje CLS:
- Obrazki bez width/height
- Fonty wczytujące się z opóźnieniem (FOIT/FOUT)
- Reklamy i embedy bez zarezerwowanego miejsca
- Dynamicznie ładowany content (infinite scroll, powiadomienia)
- Web fonty
Jak znaleźć CLS w DevTools
DevTools > Performance. Nagraj ładowanie strony. W sekcji Experience zobaczysz czerwone kropki. Każda to layout shift. Najedź, aby zobaczyć który element się przesunął.
Lub w Console:
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
console.log('CLS shift:', entry.value);
console.log('Elementy:', entry.sources?.map((s) => s.node));
}
}
}).observe({ entryTypes: ['layout-shift'], buffered: true });Jak naprawić CLS w 5 minut
Krok 1: Zawsze podawaj width i height na obrazkach
<!-- ZŁO - przeglądarka nie zna rozmiaru przed załadowaniem -->
<img src="photo.jpg" alt="Zdjęcie" />
<!-- DOBRZE - przeglądarka rezerwuje miejsce -->
<img src="photo.jpg" alt="Zdjęcie" width="800" height="600" />Krok 2: aspect-ratio dla responsywnych obrazków
<!-- CSS -->
.responsive-img {
width: 100%;
height: auto;
aspect-ratio: 16 / 9;
}
<!-- HTML -->
<img
src="photo.jpg"
alt="Zdjęcie"
width="1600"
height="900"
class="responsive-img"
/>Krok 3: Zarezerwuj miejsce na reklamy i embedy
<!-- ZŁO - reklama się załaduje i przesunie wszystko -->
<div id="ad-container"></div>
<!-- DOBRZE - miejsce zarezerwowane -->
<div id="ad-container" style="min-height: 250px; background: #f5f5f5;">
<!-- reklama się tu załaduje -->
</div>Krok 4: Font-display: swap lub optional
/* ZŁO - font-display: block ukrywa tekst do momentu załadowania fontu */
@font-face {
font-family: 'Mój Font';
src: url('font.woff2') format('woff2');
font-display: block;
}
/* DOBRZE - swap pokazuje fallback font natychmiast */
@font-face {
font-family: 'Mój Font';
src: url('font.woff2') format('woff2');
font-display: swap;
}
/* LEPiEJ - optional jeśli font nie jest krytyczny */
@font-face {
font-family: 'Mój Font';
src: url('font.woff2') format('woff2');
font-display: optional;
}Krok 5: Preload krytycznych fontów
<link rel="preload" href="/fonts/critical.woff2" as="font" type="font/woff2" crossorigin />Krok 6: size-adjust w @font-face (opcjonalnie, zaawansowane)
Jeśli Twój fallback font ma zupełnie inną szerokość niż web font, tekst będzie skakał nawet przy swap.
@font-face {
font-family: 'Fallback';
src: local('Arial');
size-adjust: 107%; /* Dostosuj do szerokości głównego fontu */
}Checklist: Popraw Core Web Vitals w 30 Minut
Minuta 0-5: Diagnostyka
- □Otwórz PageSpeed Insights i wpisz URL strony
- □Sprawdź sekcję "Core Web Vitals Assessment"
- □Zapisz aktualne wartości LCP, INP, CLS
- □Otwórz DevTools > Performance, znajdź element LCP
- □Włącz throttling do "Slow 4G" w DevTools Network tab
Minuta 5-10: LCP
- □Dodaj
fetchpriority="high"do LCP obrazka - □Jeśli LCP to background-image, zamień na
<img>z preload - □Dodaj
widthiheightdo wszystkich obrazków - □Skonwertuj LCP obrazek do WebP lub AVIF
- □Sprawdź czy obrazek nie jest większy niż potrzeba (max 2x rozdzielczość ekranu)
Minuta 10-20: INP
- □Zainstaluj web-vitals:
npm install web-vitals - □Dodaj monitoring INP (kod wyżej)
- □Znajdź najwolniejszą interakcję w DevTools Performance
- □Dodaj
scheduler.yield()lubsetTimeout(..., 0)do długich pętli - □Zastosuj throttle/debounce na scroll i resize
- □Popraw layout thrashing (read first, write later)
- □W React: użyj
useTransitiondla ciężkich aktualizacji
Minuta 20-25: CLS
- □Dodaj
widthiheightlubaspect-ratiodo wszystkich obrazków - □Zarezerwuj
min-heightna kontenery reklam i embedów - □Zmień
font-display: blocknaswapluboptional - □Preload krytyczne fonty
- □Sprawdź dynamicznie ładowany content (czy ma zarezerwowane miejsce)
Minuta 25-30: Weryfikacja
- □Uruchom Lighthouse w DevTools (Mobile)
- □Sprawdź czy LCP < 2.5 s, INP < 200 ms, CLS < 0.1
- □Uruchom test ponownie z "Slow 4G" throttling
- □Sprawdź w PageSpeed Insights czy wynik się poprawił
- □Wrzuć na produkcję i sprawdź CrUX za 2-3 tygodnie
Narzędzia, których używam na co dzień
Do szybkiego testu:
- PageSpeed Insights — najszybszy sposób, żeby zobaczyć CrUX + syntetyczne dane
- WebPageTest — szczegółowe analizy z różnych lokalizacji i urządzeń
- Chrome DevTools > Performance — do debugowania konkretnych problemów
Do monitoringu:
- web-vitals library — JavaScriptowa biblioteka do śledzenia CWV w czasie rzeczywistym
- CrUX Dashboard (Data Studio) — dane rzeczywistych użytkowników
- SpeedCurve lub Calibre — płatne narzędzia do ciągłego monitoringu
Do optymalizacji:
- Squoosh — kompresja obrazków (WebP, AVIF)
- BunnyCDN lub Cloudflare — CDN z automatyczną optymalizacją obrazków
- Vite lub esbuild — szybkie bundlowanie JS bez zbędnego kodu
Najczęstsze błędy, które widzę u klientów
Błąd 1: "Moja strona ma 99 w Lighthouse, więc jest super"
Lighthouse to syntetyczny test na idealnym połączeniu. CrUX to dane rzeczywistych użytkowników z Polski na 3G i starych Xiaomi. Te dane mogą się różnić o 200%. Zawsze patrz na CrUX jako źródło prawdy.
Błąd 2: "Dodałem lazy loading do wszystkich obrazków"
Lazy loading na obrazku LCP to strzał w stopę. LCP musi załadować się natychmiast, nie po przewinięciu. Lazy loading dla LCP dodaje 200-1000 ms. Zawsze sprawdzaj czy LCP nie ma loading="lazy".
Błąd 3: "Używam najnowszego frameworku, więc jest szybko"
Next.js, Nuxt, SvelteKit... wszystkie mogą być wolne jeśli wysyłasz 500 KB JS na stronę. Framework nie zastąpi optymalizacji. Code splitting, lazy loading komponentów i tree shaking to Twoja odpowiedzialność.
Błąd 4: "Zoptymalizowałem wszystko, ale hosting jest w USA"
TTFB (Time to First Byte) to często największy problem. Jeśli Twój serwer jest w USA, a klienci w Polsce, masz dodatkowe 100-150 ms, których nie przeskoczysz optymalizacją kodu. Użyj CDN. Netlify, Vercel, Cloudflare Pages mają edge nodes w Europie.
Podsumowanie
Core Web Vitals w 2026 roku to nie jest opcja. To wymóg. Google używa CrUX jako jednego z czynników rankingowych, a INP zastąpił FID jako znacznie bardziej rygorystyczna metryka.
Ale nie ma co panikować. Większość problemów da się rozwiązać w 30 minut:
- LCP: fetchpriority="high" + preload + WebP + width/height
- INP: podziel długie zadania, użyj scheduler.yield(), throttle eventy
- CLS: width/height na obrazkach, zarezerwuj miejsce na reklamy, font-display: swap
Nie musisz być ekspertem od wydajności. Musisz tylko wiedzieć gdzie szukać i jakie zmiany robić.
Zacznij od jednej strony. Zmierz przed i po. Wdróż na produkcję. Potem kolejna strona. W ciągu miesiąca Twoja cała strona będzie zielona.
Powodzenia i niech Twój INP będzie zawsze poniżej 200 ms.
Porozmawiajmy o Twojej stronie.
Napisz, czym zajmuje się Twoja firma i czego potrzebujesz. Ustalimy zakres prac i wycenę.
Napisz o swojej stronie

