Analiza szybkości i Core Web Vitals - raport przykładowy
Przykładowy, anonimizowany audyt z danymi ilustracyjnymi. To pełny dokument w formacie i objętości, w jakiej dostajesz realny, ręczny audyt - liczby i nazwy są reprezentatywne (przykładowe), a struktura i poziom szczegółowości jak w prawdziwym raporcie.
Metryka raportu i punkt wyjścia
Zakres audytu i metodologia badania
Audyt szybkości i Core Web Vitals zrobiłem 22 czerwca 2026 roku na portalu e-commerce. Przejrzałem 265 stron - główne kategorie produktów, strony informacyjne i całą ścieżkę zakupową. Sprawdzam dwie rzeczy naraz: dane od prawdziwych użytkowników z Google Chrome User Experience Report (CrUX) oraz pomiary laboratoryjne (Lab) z Google Lighthouse i WebPageTest. Jedno bez drugiego daje fałszywy obraz.
Testy laboratoryjne odpalałem na urządzeniach, które symulują realne warunki. Mobile: sieć 4G (pobór 1.6 Mb/s, wysyłanie 750 kb/s, opóźnienie 50ms) i czterokrotne spowolnienie procesora (CPU 4x throttle) - tak działa typowy smartfon ze średniej półki. Desktop testowałem na nieobciążonym procesorze, ale tej samej sieci 4G, żeby wyniki dało się porównać. Dane CrUX obejmują 28 dni przed audytem, zebrane od 2 847 realnych użytkowników, przy aktywnym mobile-first indexingu Google.
Baseline metryki - obecny stan techniczny
Stan wyjściowy pokazuje dużą przepaść między mobile a desktopem i sporo problemów strukturalnych, które ciągną Core Web Vitals w dół:
| Metryka | Wartość Lab (Mobile) | Wartość CrUX (Mobile) | Desktop | Cel | Status |
|---|---|---|---|---|---|
| LCP (s) | 3,8 | 4,2 | 2,1 | <2,5 | 🔴 Krytyczne |
| INP (ms) | 248 | - | 45 | <100 | 🔴 Krytyczne |
| CLS | 0,18 | 0,22 | 0,08 | <0,1 | 🟡 Słabe |
| TTFB (ms) | 520 | - | 380 | <600 | 🟡 Słabe |
| FCP (s) | 1,5 | - | 0,9 | <1,8 | ✅ Akceptowalne |
Largest Contentful Paint (LCP) to 4,2 sekundy na mobile w danych polowych - 68% ponad cel 2,5 sekundy. Desktop robi 2,1 sekundy, czyli dwa razy lepiej. W teście Lab głównym elementem LCP jest grafika hero na stronie głównej (2,1 MB w JPG), serwowana bez optymalizacji. Waterfall pokazuje LCP dopiero o 3,8 sekundy, a DOMContentLoaded o 2,1 sekundy - to znaczy, że rendering blokuje JavaScript (5 plików blokujących, opóźnienie 1,2 sekundy).
Interaction to Next Paint (INP) wynosi 248 milisekund na mobile, 2,5x powyżej celu <100ms. Interfejs reaguje z opóźnieniem. 60% audytowanych stron przekracza próg „Poor” dla INP. Winny jest JavaScript: 840 milisekund parse/compile time, z czego 520 ms idzie na samo wykonanie kodu. Profiling w DevTools pokazał, że main thread jest zajęty 85% czasu, zwłaszcza podczas interakcji. Skrypty third-party (8 plików) odpowiadają za sporą część tego: Google Analytics (+240ms), Facebook Pixel (+180ms) i reCAPTCHA (+320ms). Do tego architektura SPA na Reakcie daje hydration time 1,8 sekundy z Main Thread Blocking 156 milisekund - kolejny czynnik psujący responsywność.
Cumulative Layout Shift (CLS) na poziomie 0,22 w CrUX (ponad dwa razy powyżej celu 0,1) oznacza skaczący layout. Powody: brak preloadu webfontów (4 rodziny czcionek, FOUT 200-400ms), brak ustalonej wysokości dla elementów dynamicznych i renderowanie treści poniżej folda bez rezerwacji miejsca (szczególnie produkty doładowywane asynchronicznie).
Time To First Byte (TTFB) to 520 milisekund: server rendering 180ms (w porządku), network latency 340ms (sieć mobilna) i cache miss Redis 42%. Tylko 22% stron ma poprawny cache-control; 78% ma headery byle jakie. Cache hit na CDN to 38% - strategia cachowania nie działa.
Zużycie zasobów i optymalizacyjne potencjały
Po zasobach widać, gdzie leżą największe rezerwy:
JavaScript: Bundle łączny 862 KB, w 23 osobnych plikach (powinno być 3-5). Pięć plików jest render-blocking (1,2 sekundy opóźnienia LCP). CSS-in-JS dokłada 140ms do FCP i 180ms do LCP. Osiem skryptów third-party generuje 740ms łącznego opóźnienia (Google Analytics 240ms, Facebook Pixel 180ms, reCAPTCHA 320ms). Code-splitting może oszczędzić 240 KB.
CSS: Rozmiar 284 KB, z czego 98 KB to render-blocking CSS. Średnia głębokość specificity to 6 poziomów, 23 reguły powodują slow-down. Optymalizacja CSS-in-JS i usunięcie martwego CSS zwolni 65 KB.
HTML: Średni rozmiar 285 KB - mocno powyżej benchmarku branżowego (80-120 KB). Bez minifikacji oszczędność: 85 KB.
Obrazy: Średnio 3,2 MB na stronę, z czego 68% (2,1 MB) to nieoptymalizowany plik hero (JPG 2,1 MB). WebP obsługiwany na 12% stron (de facto brak wdrożenia), AVIF: 0%. Konwersja hero do AVIF zmniejszy go o 68%. Łączna oszczędność: 1,9 MB na stronę, czyli średnio 420 KB zamiast obecnego (optymalizacja 68%).
Kompresja: Gzip na 78% stron (średnia oszczędność 35%). Brotli: ledwie 2% stron (potencjał +220 KB średnio). Po wdrożeniu Brotli łączna redukcja: 185 KB.
Liczba HTTP requestów: 87 żądań (64 dla assetów statycznych), cel: 52 (redukcja o 40%). Zmniejszenie bundli zejdzie do 58 requestów w ciągu 30 dni.
Wpływ na biznes i korzyści konwersji
Korelacja między prędkością ładowania a konwersją wynosi 0.74 - związek jest silny. Obecny conversion rate na mobile (1,8%) jest o 58% niższy niż na desktopie (4,2%). Bounce rate na mobile przy LCP >3s to 62%, a przy LCP <2s spada do 28% - różnica 34 punktów procentowych. Dla strony z 2,4 milionami wizyt miesięcznie ta przepaść kosztuje szacunkowo 42,000 PLN miesięcznie.
Z badań branżowych i wewnętrznej analityki widać, co dają poszczególne ruchy: zmniejszenie LCP o 1 sekundę to lift konwersji +2,8%, czyli dla bieżącego ruchu mobile (1,2M wiz/mies.) = 20,160 dodatkowych konwersji rocznie. Zejście INP poniżej 100ms podnosi sessyjność o +1,4%, stabilizuje engagement i o 12% zmniejsza powroty do strony głównej.
Cel biznesowy to wzrost konwersji +18% i redukcja bounce rate -28% w 90 dni. Da się go osiągnąć przez:
- Osiągnięcie LCP <2,5s = +8% konwersji mobilnej
- Redukcja INP <100ms = +4% sessyjności
- Stabilizacja CLS <0,1 = +2% retention
- Razem target +14% (rozpiętość 90 dni pozwala na dodatkowe +4% z organic reach improvement)
Prioryty optymalizacyjne i terminy realizacji
P1 (Krytyczne, wpływ 30 dni):
- Optymalizacja hero image: konwersja do AVIF, lazy-loading niewidocznych grafik (-1.2s LCP) - Efekt: LCP 3.8s → 2.6s
- Usunięcie render-blocking JavaScript (5 plików) - Efekt: +0.8s FCP, +1.2s LCP
- Redukcja third-party skryptów: asynchronizacja Google Analytics i Facebook Pixel - Efekt: -320ms LCP
- Implementacja Service Workera dla static assets - Efekt: +62% cache hit, -340ms TTFB
P2 (Wysokie, wpływ 60 dni):
- Code-splitting React bundle: zmniejszenie initial JS z 862KB do 520KB - Efekt: -140ms FCP, -180ms parse time
- Preload webfontów, wdrożenie font-display: swap - Efekt: -200ms FOUT, +0.05 CLS improvement
- Optymalizacja CSS (usunięcie dead code, redukcja specificity) - Efekt: -98ms rendering
P3 (Średnie, wpływ 90 dni):
- Migracja na Brotli compression - Efekt: -185KB TTFB
- Database query optimization: cache Redis miss -42% → 15% - Efekt: -300ms server response
Szacunkowe efekty końcowe: LCP 3.8s → 2.0s (zmniejszenie 47%), INP 248ms → 85ms (zmniejszenie 66%), CLS 0.22 → 0.09 (zmniejszenie 59%), konwersja mobilna +18% = dodatkowo 216,000 PLN przychodów rocznych.
Streszczenie zarządcze
Audyt pokazał krytyczne problemy z Core Web Vitals na mobile, które bezpośrednio biją w wyniki biznesowe. Sprawdzałem dane realnych użytkowników (CrUX) z 2 847 urządzeń mobilnych, pomiary laboratoryjne w Chrome DevTools oraz diagnostykę zasobów serwera. Najgorsze odkrycie: 48% stron dostaje ocenę „Poor” w Core Web Vitals na mobile, podczas gdy na desktopie problem dotyczy tylko 12% witryn. Ta różnica nie jest przypadkowa - pokrywa się z przepaścią w dochodach: konwersje mobilne to 1,8% wobec 4,2% na desktopie, a korelacja prędkości z konwersją wynosi 0,74.
Strata przychodu z obecnych problemów to około 42 000 PLN miesięcznie przy 2,4 milionach wizyt. Główny hamulec to Largest Contentful Paint (LCP), który na mobile wynosi 4,2 sekundy - daleko powyżej celu <2,5 sekundy. Ten parametr odpowiada za 62% bounce rate, gdy strona ładuje się dłużej niż 3 sekundy, wobec 28% przy czasach poniżej 2 sekund. Drugi krytyczny problem to Interaction to Next Paint (INP) na poziomie 248 milisekund - 60% stron przekracza próg akceptowalności (<100ms), więc interfejs sprawia wrażenie zamulonego. Trzeci czynnik to Cumulative Layout Shift (0,22 w danych rzeczywistych) - treść skacze i utrudnia konwersję.
| Metrika | Bieżący stan | Cel | Luka | Mobilność vs Desktop |
|---|---|---|---|---|
| LCP (Lab) | 3,8s | 2,5s | -1,3s (-35%) | 4,2s vs 2,1s (2x) |
| INP | 248ms | <100ms | -148ms (-60%) | 312ms vs 45ms |
| CLS | 0,22 (Field) | 0,1 | -0,12 (-120%) | 0,18 (Lab) vs 0,22 (Field) |
| TTFB | 520ms | <300ms | -220ms | Backend 180ms, Network 340ms |
| Bounce rate (LCP >3s) | 62% | 28% | -34pp | Przy <2s |
Źródła problemu: Diagnostyka wskazuje trzy główne przyczyny. Obrazy nie są zoptymalizowane - średnio 3,2 MB na stronę, z czego 68% da się zmniejszyć, a WebP jest na zaledwie 12% przypadków. Największy element strony (hero image) to 2,1 MB w JPG, choć konwersja do AVIF ścięłaby go o 68%. Druga sprawa to JavaScript: 862 KB nieskompresowanego kodu w 23 plikach, z czego 5 blokuje renderowanie (opóźnienie 1,2s), a 8 skryptów third-party (Google Analytics +240ms, Facebook Pixel +180ms, reCAPTCHA +320ms) zjada 740 milisekund. Sam parse i execute tego kodu trwa 840 milisekund. Trzecia rzecz to Time To First Byte 520 milisekund - backend odpowiada w 180 ms, a 340 ms pochłania sieć. Cache hit CDN to zaledwie 38%, tylko 22% stron ma poprawne nagłówki cache-control. Do tego baza odpowiada średnio w 145 ms, ale najwolniejszy endpoint (/api/products) potrzebuje 480 ms, a Redis ma miss rate 42%.
Plan naprawy - trzy fazy: Działania podzieliłem na trzy fazy z priorytetami. P1 - optymalizacja obrazów (30 dni, szacunkowy zysk: -1,2s LCP): konwersja hero image i reszty assetów do AVIF z fallbackami WebP, lazy loading, zejście ze średniego rozmiaru z 420 KB do 140 KB na obraz. Oszczędność: 1,9 MB na stronę. P2 - refaktor JavaScript (45 dni, szacunkowy zysk: -0,8s TTFB + FCP): code splitting React bundla z 862 KB na moduły ~300 KB, async/defer dla 5 render-blocking skryptów, preload krytycznych zasobów, sandbox dla third-party, Brotli na 98% stron (zamiast 2%). Oszczędność: ~220 KB na stronie. P3 - poprawa TTFB i cachingu (60 dni, szacunkowy zysk: -0,3s TTFB): optymalizacja zapytań do bazy, zmiana Redis cache strategy z 42% miss na <15%, edge caching CDN z 38% na >75% hit rate, Service Worker dla statycznych zasobów, preload czcionek.
Czego się spodziewam po 90 dniach: LCP spadnie do 2,1s (mobile), INP do 120ms (to wciąż wymaga dalszej pracy), CLS ustabilizuje się na <0,1, bounce rate spadnie z 62% do ~32%, a mobile conversion rate wzrośnie z 1,8% do 2,7% (+2.8% lift). Przy 2,4 milionach wizyt miesięcznie i średnim AOV przychód wzrośnie o ~50 000 PLN. Koszt wdrożenia to 35-50 tys. PLN (zespół wewnętrzny 3-4 tygodnie + ewentualnie audyt Senior Frontend), co daje ROI ~8-12 w ciągu 6 miesięcy. Bez działań problem będzie się pogłębiać - konkurencja przyspiesza, a algorytm Google już premiuje Core Web Vitals.
Top problemy z priorytetami
Audyt objął 265 stron sklepu - Chrome DevTools, Google PageSpeed Insights, Google Lighthouse Lab oraz dane CrUX z ostatnich 28 dni (2,847 realnych użytkowników mobilnych i desktopowych). Problemy krytyczne wybierałem po korelacji metryk wydajności z biznesem: bounce rate (+34pp różnicy między LCP >3s a <2s), konwersja (mobile 1.8% vs desktop 4.2%, korelacja 0.74) i strata przychodu rzędu 42,000 PLN/miesiąc.
| Priorytet | Problem | Wpływ techniczny | Koszt biznesowy | Rozwiązanie | Efekt oczekiwany |
|---|---|---|---|---|---|
| P0 | Obrazy nieoptymalizowane | 1.9MB potencjału zmniejszenia/stronę; 68% bez kompresji; 0% w formacie WebP/AVIF | -2.8% konwersji na mobile; utrata ~29,600 PLN/miesiąc | 1) Konwersja hero image (2.1MB JPG) do AVIF (-68%); 2) Implementacja adaptatywnych srcset; 3) Lazy-loading dla below-fold; 4) Mozjpeg/oxipng na CI/CD | LCP -1.2s; obrazy 340KB zamiast 2.1MB; +2.8% konwersji |
| P0 | TTFB za wysoki | 520ms całkowity; serwer 180ms (akceptowalny); sieć 340ms (CDN issue); cache hit rate 38% zamiast >80% | Bounce rate +18pp; przesunięcie pozostałych metryk (LCP 3.8s zamiast <2.5s) | 1) Audyt cache-control headers (78% stron sub-optimalnie); 2) Implementacja Brotli (obecnie 2%; potencjał -220KB); 3) Redis cache dla endpoint’ów API (np. /products: 480ms → <150ms); 4) CDN cache rules hardening | TTFB ≤300ms; cache hit >80%; LCP -0.8s dzięki szybszemu TTFB |
| P1 | Render-blocking CSS | 98KB CSS blokuje FCP; +180ms do LCP | Delayed First Paint → brak wizualnej odpowiedzi przez 1.5s+ | 1) Critical CSS inlining (<14KB above fold); 2) Defer non-critical CSS via media queries; 3) Inline SVG sprite zamiast CSS background; 4) PurgeCSS: 284KB CSS → ~120KB (usunięcie unused rules) | FCP 1.0s zamiast 1.5s; LCP 2.8s zamiast 3.8s |
| P1 | Third-party JavaScript (8 plików) | 740ms sumarnie; Google Analytics +240ms, Facebook Pixel +180ms, reCAPTCHA +320ms; parse/compile 840ms | INP degradacja do 248ms (target <100ms); 60% stron powyżej progu; bounce rate +12pp | 1) Web Worker dla Google Analytics; 2) Defer reCAPTCHA do POSTu formularza (lazy-trigger); 3) Facebook Pixel wrap w setTimeout(…, 5000); 4) Monitorowanie Core Web Vitals Trade Samą alternatywą: Plausible Analytics (~40ms zamiast 240ms) | INP <150ms; eliminacja 520ms junk; +1.4% sessions/user |
| P1 | JavaScript main thread blokada | 312ms INP na mobile; 85% czasu main thread zajęty parsing/execution; hydration 1.8s (React SPA bez SSR) | Brak interaktywności; delay do-click-to-response; 62% bounce rate przy LCP >3s | 1) Implementacja SSR (Next.js w trybie server rendering); 2) Hydration 1.8s → <400ms przez komponentyzację; 3) Code splitting: 862KB JS bundla → chunks po 120-180KB; 4) Web Worker dla computations (sortowanie, filtrowanie w Product Listing) | INP 45-80ms (desktop-like), głównie input delay <50ms |
| P2 | Font loading FOUT | 4 webfonty bez preload; FOUT delay 200-400ms; FCP degradacja o brak optymalizacji | Invisible text flash; CLS +0.08 (total 0.18); UX degradacja | 1) font-display: swap (zamiast block); 2) <link rel="preload"> dla 2 krytycznych fontów; 3) Local font fallback (system stack); 4) Wczytanie w preconnect do Google Fonts | FOUT <100ms; CLS -0.08; spostrzegana FCP szybsza |
| P2 | Backend API opóźnienia | Avg 145ms, ale /products: 480ms, P95: 620ms; Redis miss 42%; 8% requests trafia limit rate | Delayed page hydration; timeout dla slow users; load spike powoduje failure | 1) Redis cache warmer dla /api/products query (cacheability: 60min); 2) Database index audit (N+1 query pattern likely); 3) Connection pooling (PostgreSQL max_connections); 4) GraphQL batching zamiast REST REST | /products <150ms; Redis hit >90%; P95 <250ms |
| P2 | CLS (Cumulative Layout Shift) | Lab: 0.18; Field: 0.22 (>0.1 poor); głównie hero image lazy-load trigger i image dimensions missing | Bounce rate +8pp (UX jarring); mobile users abandon; paid search Quality Score penalty | 1) Explicit width i height na <img> tags; 2) aspect-ratio CSS property dla containers; 3) loading="lazy" z margin-bottom placeholder; 4) Intersection Observer dla soft-load (preload 2s przed scroll) | CLS <0.1; field-stable; +3% mobile engagement |
| P3 | Liczba HTTP requests (87 total) | 68 requests to assets; target redukcja do 52; waterfal serializacja koszt ~300ms | Redundant round-trips; slow networks (3G) cierpi; resource contention | 1) HTTP/2 push dla critical assets (CSS, main.js); 2) SVG sprite sheet zamiast 12 PNGs; 3) Font subsetting (wciąż 4 webfonty → 2); 4) CSS/JS concatenation (nie bundle, ale przynajmniej group) | 52 requests; waterfall -250ms; LCP -0.4s |
| P3 | React hydration brak SSR | 1.8s hydration; 156ms TBT (Total Blocking Time); zero pre-rendered HTML | Blank page do 1.5s; interaktywność delayed; mobile users experience “frozen” app | 1) Next.js App Router (SSR by default); 2) Selective hydration (interactive-only); 3) getStaticProps dla catalog stron; 4) Streaming SSR (React 18 Suspense) | Hydration <400ms; FCP 0.8s; interactive <1.2s |
| P3 | CSS specificity i unused rules | Avg 6 levels specificity; 23 high-cost rules; 284KB CSS z 45% waste | Selector re-evaluation overhead; +140ms FCP via CSS-in-JS; bundle bloat | 1) PurgeCSS/Tailwind CSS (atomic, ~40KB); 2) CSS refactor: max 3 levels specificity; 3) Remove unused @media + legacy class rules; 4) Critical CSS tooling (inline <14KB) | CSS 120KB zamiast 284KB; FCP +0.3s |
| P3 | Brotli nie wdrożony | Aktualnie 2% stron; tylko gzip (78%); Brotli savings ~220KB average | 220KB/stronę = 6% dodatkowego zmniejszenia vs gzip; 42M+ monthly visitors = 9.2TB wasted bandwidth/miesiąc | 1) Enable Brotli w nginx/Apache (poziom 11); 2) Accept-Encoding: br w CDN config; 3) Verify client browser support (>95% coverage); 4) Monitor via Accept-Encoding header logs | Payload -220KB/stronę; transfer time -18%; +0.2s LCP relief |
Podsumowanie wpływu: P0 (obrazy + TTFB + CSS) ścina LCP z 3.8s do ~2.2s w 30 dni (quick-win 1.2s). P1 (third-party JS + hydration) obniża INP z 248ms do <100ms, co podbija mobile conversion rate o 2.8% (szacunkowo +117,600 PLN/miesiąc). Core Web Vitals “Poor” spadają z 48% (mobile) do <15% w 90 dni, „Good” na 85%+ stron - to plasuje sklep w top 15% konkurencji na mobile i poprawia ranking organiczny w Search Console średnio o +12 pozycji dla słów średnio-konkurencyjnych.
Metodyka badania
Audyt prowadziłem warstwowo: testy laboratoryjne, dane realnych użytkowników i pomiary funkcjonalne. Dopiero razem dają pełny obraz Core Web Vitals i tego, co psuje doświadczenie.
Testy laboratoryjne - Lighthouse 12
Podstawą były kontrolowane testy w Google Lighthouse 12 w dwóch konfiguracjach sprzętowych. Na mobile symulowałem Pixel 6 w pięciu powtórzeniach z throttlingiem sieci (profil 4G, opóźnienia 150ms, zmienna prędkość pobierania) i CPU x4 slowdown - tak wygląda praca na średnich i słabszych procesorach. Dla desktopu zrobiłem 5 testów w Chrome ze standardowymi profilami sieci (Fast 3G equiv.) i limitem CPU x2. Testy odpalałem dwukrotnie - zaraz po wyczyszczeniu cache’a i po 3 dniach pracy - żeby pokazać, jak cache wpływa na TTFB i FCP.
Wyniki laboratoryjne: średnia LCP (Largest Contentful Paint) 3,8 sekundy, czyli mocno powyżej celu (<2,5s), do ścięcia o 35% w 90 dni. Desktop wypadł znacznie lepiej - LCP 2,1s - więc mobile jest 2x wolniejszy. INP (Interaction to Next Paint) osiągnął 248 ms przy docelowych <100ms; 60% stron przekroczyło próg. CLS (Cumulative Layout Shift) w labie wyniósł 0,18, ale u realnych użytkowników rośnie do 0,22 - to efekt asynchronicznego doładowywania komponentów.
Dane rzeczywistych użytkowników - Chrome UX Report
Dane CrUX z 28-dniowego okna i 2 847 realnych użytkowników zweryfikowały wyniki labowe w naturze. LCP na mobile siada na 4,2 sekundy (górny kwartyl), na desktopie trzyma 2,1s - różnica 2x potwierdza, że problem leży po stronie mobile. INP w terenie to średnio 312ms na mobile wobec 45ms na desktopie, co wskazuje na blokowanie głównego wątku JavaScript. Górny percentyl (P95) CrUX pokazuje LCP do 6,8s dla 5% sesji mobilnych - stąd te skrajne przypadki frustracji.
Analiza zasobów i ścieżki krytycznej
Waterfall z WebPageTest pokazał kolejność ładowania: DOMContentLoaded w 2,1s (głównie czekanie na JavaScript), pełny Load Event w 4,8s. First Contentful Paint (FCP) wypada w 1,5s, ale Largest Contentful Paint (LCP) to 3,8s - różnica 2,3 sekundy to opóźnione ładowanie zdjęcia głównego (2,1MB w JPG). Analiza Resource Timing pokazała:
- TTFB (Time To First Byte): 520ms - rozkład: serwer 180ms (backend processing) + sieć 340ms (latencja CDN/routing). Cache hit na CDN osiąga jedynie 38%, pozostałe requesty trafiają do origin.
- HTML: 285KB średnio na stronę (unminified, z wbudowanym stylem)
- JavaScript bundel: 862KB (głównie React + vendor dependencies); 5 plików blokujących render przesyłane synchronicznie powoduje dodatkowe opóźnienie 1,2s
- CSS: 284KB łącznie; 98KB render-blocking CSS zanim będzie możliwy FCP
- Obrazy bez optymalizacji: średnio 3,2MB na stronę; WebP dostępny dla 12% zasobów, AVIF w ogóle nie wdrożony
- 23 pliki JavaScript, z czego 8 to third-party (Google Analytics +240ms, Facebook Pixel +180ms, reCAPTCHA +320ms razem +740ms overhead)
| Metryka | Wartość | Benchmark (P75) | Status |
|---|---|---|---|
| LCP (Lab) | 3,8s | <2,5s | 🔴 POOR |
| LCP (Field) mobile | 4,2s | <2,5s | 🔴 POOR |
| LCP (Field) desktop | 2,1s | <2,5s | 🟢 GOOD |
| INP | 248ms | <100ms | 🔴 POOR |
| CLS | 0,18 (lab) / 0,22 (field) | <0,1 | 🟡 NEEDS WORK |
| TTFB | 520ms | <600ms | 🟡 BORDERLINE |
| FCP | 1,5s | <1,8s | 🟢 GOOD |
Profiling JavaScript i main thread
Chrome DevTools Performance tab (10 sesji rejestracji) pokazał, że parse, compile i execution JavaScriptu zajmują łącznie 840ms - z czego 520ms to czysty execution, reszta to bottleneck kompilacji V8. Main thread jest zablokowany JavaScriptem przez 85% czasu ładowania, więc przeglądarka nie nadąża reagować na interakcje. Framework CSS-in-JS (Emotion) dokłada +140ms do FCP i +180ms do LCP względem zwykłego inline/optimized CSS. Hydration (SSR: 0%, czysty React SPA) trwa 1,8s, a TBT (Total Blocking Time) podczas hydracji sięga 156ms.
Coverage tab pokazał, że 62% kodu CSS nie jest używane na pierwszej stronie, a 44% JavaScriptu ładuje się dla stron, których kod nie jest potrzebny (kod źle podzielony). Font loading ściąga 4 webfonty bez preload (0% wdrożenia), co daje FOUT (Flash of Unstyled Text) 200-400ms opóźnienia.
Monitoring ciągły - SpeedCurve i RUM
SpeedCurve continuous monitoring (codzienna aktualizacja przez 30 dni) pokazał destabilizację: LCP wahał się w przedziale 3,4-4,1s z lekkim trendem wzrostowym. Real User Monitoring z BoomerangJS potwierdził korelację 0,91 między deploymentami a pogorszeniem LCP. RUM wyłapał też, że na tej samej sieci (4G throttling) wariancja wynosi ±0,8s zależnie od pory dnia - w godzinach szczytu (19:00-21:00) wartości rosną nawet do 5,2s przez obciążenie CDN.
Badanie percepcji użytkownika
Ankieta na 304 użytkownikach (próba reprezentatywna dla 2,4M monthly visits) pokazała wyraźną zależność prędkości i zadowolenia. 62% użytkowników mobilnych zgłaszało “slow experience” przy LCP > 3s; tylko 28% deklarowało komfort przy LCP < 2s. Bounce rate na mobile przy LCP > 3s sięga 62%, a przy LCP < 1,5s spada do 28% - różnica +34 pp. Korelacja LCP z mobile conversion rate to 0,74 (mocna zależność): przy obecnych 1,8% na mobile vs 4,2% na desktopie przyspieszenie o 1s mogłoby teoretycznie podnieść mobilną konwersję o +2,8pp (rozkład 28,6% do 31,4%).
Analiza strony serwerowej i cache’owania
Database query avg to 145ms, najwolniejszy endpoint /api/products zwraca w 480ms (P95). Redis ma miss rate 42% - czyli prawie połowa żądań idzie do bazy zamiast do cache’a. SSR: 0% (czysty SPA React), hydration time 1,8s. TTFB rozkłada się na 180ms backend + 340ms sieć. CDN cache headers są poprawne tylko na 22% stron; 78% ma sub-optimal cache-control (brak max-age albo private zamiast public), stąd niski 38% cache hit.
Zakres wyłączeń i margines błędu
Audyt nie obejmował testów ad network’ów (Google Ads, DoubleClick) ani wariantów AMP (nie są używane na stronie). Margin błędu dla lab testing to ±2%, dla danych polowych (CrUX) ±5% przy sample size 2847. Margin błędu dla szacunku przychodu (~42,000 PLN/miesiąc straty przy 2,4M monthly visits) to ±8% przez sezonowe wahania konwersji.
Performance budget i optymalizacyjne quick-wins
Performance budget jest przekroczony: JS +240KB ponad limit, Images +1.8MB, CSS +48KB. LCP da się ściąć o 1,2s w 30 dni bez głębokich refaktorów (optymalizacja obrazów, redukcja third-party, minifikacja CSS-in-JS). Przychód tracony przez szybkość to ~42,000 PLN/miesiąc (przy 2,4M monthly visits, 1,8% current conversion rate, AOV 180 PLN i delcie konwersji 0,74 correlation).
Rekomendacje do wdrożenia - Matryca priorytetów
| Działanie | Narzędzie weryfikacji | Priorytet | Oczekiwany efekt LCP | Szacunkowy ROI |
|---|---|---|---|---|
| Optimizacja obrazów + WebP/AVIF | Lighthouse Coverage | P1 | -0,8s | +1.9MB/page savings |
| Redukcja third-party JS (Analytics, Pixel, reCAPTCHA async load) | RUM + BoomerangJS | P1 | -0,6s | -740ms overhead |
| Implementacja Redis cache layer optimization (reduce miss 42% → 15%) | Backend profiler | P1 | -0,3s | +23% faster API |
| Split JavaScript bundla + Code splitting na level route’ów | Lighthouse + Webpack Bundle Analyzer | P2 | -0,4s | -240KB JS payload |
| CSS-in-JS → optimized CSS inline/preload system | Chrome DevTools | P2 | -0,18s | -140ms FCP impact |
| Implementacja Service Worker + static asset caching (offline first) | Lighthouse | P2 | -0,2s (repeat visits) | 0% cache miss |
| Font preload + font-display: swap strategy | Lighthouse | P3 | -0,25s | -200-400ms FOUT |
Audyt potwierdza: 127 stron z 265 (48%) jest w kategorii “Poor” Core Web Vitals na mobile. Szybkimi wdrożeniami da się ściąć 1,2s w 30 dni, co teoretycznie daje +2,8% konwersji mobilnej = ~34 000 PLN przychodu dodatkowo w miesiącu.
Analiza obrazów i mediów
Obrazy to największy drenaż łącza na tej stronie - średnio 3,2 MB na stronę, z czego 68% leży bez żadnej optymalizacji. Gorzej: elementem LCP na większości stron głównych jest hero image 2,1 MB w JPG. Waterfall w Chrome DevTools pokazał, że samo pobranie tego pliku daje +1,8 s do LCP, co bezpośrednio przekłada się na obserwowane w polu 4,2 s na mobile wobec docelowych <2,5 s. Do tego brak wariantów responsywnych: ten sam plik 2,1 MB leci na ekran 375px i 1920px, czyli czyste marnotrawstwo łącza, szczególnie na 4G (średnia poboru 340 ms).
Audyty w Lighthouse, PageSpeed Insights i lokalnej analizie w DevTools pokazały skalę problemu z formatami. WebP, średnio o 25-35% mniejszy niż JPG, jest wdrożony na 12% stron, a AVIF - format nowej generacji z redukcją 60-68% względem JPG przy tej samej jakości - nie jest używany nigdzie (0%). Chrome, Firefox i Edge wspierają oba formaty, więc rezygnacja z nich to po prostu zmarnowany potencjał. Dodatkowo 34% obrazów nie ma loading="lazy", więc przeglądarka pobiera niepotrzebnie zasoby powyżej fold-line (pierwsze 100 vh). W HTML brakuje <img srcset> - obrazy lecą w jednym wariancie, bez rozmiarów dopasowanych do viewportu. To wprost napędza obserwowany w polu CLS 0,22 - placeholdery doładowują się asynchronicznie.
Skutek biznesowy jest poważny. Każda dodatkowa sekunda LCP koreluje z 34 pp wzrostu bounce rate na mobile - przy obecnym LCP 4,2 s bounce rate to 62%, a przy LCP <2 s spada do 28%. Conversion rate na mobile to 1,8% wobec 4,2% na desktopie, a współczynnik korelacji 0,74 wskazuje prędkość jako główny driver konwersji. Przy 2,4 M wizyt miesięcznych te opóźnienia kosztują ~42 000 PLN miesięcznie. Lighthouse szacuje, że zejście LCP z 4,2 s do 3,1 s (osiągalne przez optymalizację obrazów) podniosłoby conversion rate o 2,8%, czyli potencjalnie +520 tys. PLN rocznie dla typowej witryny e-commerce.
Naprawa idzie warstwami. Najpierw konwersja hero image (2,1 MB JPG) do AVIF z wariantem WebP jako fallback: szacunkowo 680 KB po optymalizacji (redukcja 68%), co ścina LCP o ~1,2 s na 4G. Wymaga to przebudowy znaczników <picture> w HTML:
<picture>
<source srcset="hero-1920.avif 1920w, hero-1024.avif 1024w, hero-480.avif 480w" sizes="100vw" type="image/avif">
<source srcset="hero-1920.webp 1920w, hero-1024.webp 1024w, hero-480.webp 480w" sizes="100vw" type="image/webp">
<img src="hero-1920.jpg" alt="Hero" fetchpriority="high" decoding="async">
</picture>
Potem lazy-loading z blur placeholderami na wszystkich obrazach poniżej fold-line. loading="lazy" warto łączyć z decoding="async", żeby nie blokować głównego wątku. Dla elementów krytycznych (hero) stosujemy fetchpriority="high", a dla thumbnaili produktowych loading="lazy" z decoding="async". Na koniec CDN z automatyczną optymalizacją obrazów, np. Cloudinary albo Imgix - generuje warianty AVIF/WebP na żądanie i cachuje je na brzegu. Przy obecnym cache hit rate CDN 38% poprawne nagłówki cache-control (cache-control: public, max-age=31536000 dla versjonowanych assetów) podniosłyby ten procent do 85%+, ścinając TTFB z 520 ms do ~180 ms.
| Metrika | Obecna | Docelowa | Łączna oszczędność |
|---|---|---|---|
| Rozmiar obrazów na stronę | 3,2 MB | 1,3 MB | 1,9 MB (-59%) |
| LCP (mobile) | 4,2 s | 3,1 s | -1,1 s (-26%) |
| CLS (field) | 0,22 | <0,1 | -0,12 |
| Bounce rate (>3s LCP) | 62% | ~35% | -27pp |
| Conversion rate (mobile) | 1,8% | 2,6% | +0,8pp |
| CDN cache hit | 38% | 85% | +47pp |
Priorytet: P1 (krytyczny). Brak optymalizacji obrazów to główny powód, dla którego 127 z 265 stron (48% na mobile) nie dostaje oceny “Good” w Core Web Vitals. Plan wdrożenia: konwersja hero image (dni 1-3), responsive srcset na wszystkich obrazach (dni 4-7), CDN z auto-optimization (dni 8-14). Efekt: redukcja LCP o 1,1-1,2 s w 30 dni, wzrost conversion rate o 2,8%, spadek bounce rate o minimum 20pp, „Good” w Core Web Vitals na minimum 70% stron mobilnych. ROI szacunkowy: +1,45M PLN rocznie przy utrzymaniu stawek e-commerce.
Core Web Vitals - LCP (Largest Contentful Paint)
Zakres pomiaru i aktualna sytuacja
Largest Contentful Paint (LCP) mierzy czas, w którym ładuje się największy element widoczny w oknie przeglądarki. To kluczowy wskaźnik tego, jak szybko strona wydaje się gotowa, i od kwietnia 2021 roku jest oficjalnym sygnałem rankingowym Google Core Web Vitals. Pomiary z narzędzi diagnostycznych (PageSpeed Insights, WebPageTest) i danych realnych użytkowników (CrUX) pokazują wyraźne braki w optymalizacji tej metryki.
Średnia LCP w testach laboratoryjnych to 3,8 sekundy, podczas gdy próg „dobrze” Google to poniżej 2,5 sekundy. Lab i pole różnią się mocno: na mobile średnia LCP to 4,2 sekundy (CrUX), na desktopie tylko 2,1 sekundy - różnica dwukrotna pokazuje problem z optymalizacją mobile-first. Najgorsze: 48% stron mobilnych przekracza próg „Poor” (>4 sekundy), czyli niemal połowa audytowanych stron ma krytycznie słaby LCP na telefonach.
Źródła opóźnień - analiza waterfall’u
Waterfall pokazał cztery główne czynniki składające się na łączne 3,8 sekundy:
| Faza | Czas | Wpływ | Status |
|---|---|---|---|
| DNS + TCP + TLS | 300ms | Setup sieci | Akceptowalny |
| TTFB (Time To First Byte) | 520ms | Backend + network | Blokujący |
| Hero image fetch | 800ms | Brak preload + JPG | Krytyczny |
| Font blocking + render | 200-400ms | Webfonty bez optymalizacji | Znaczący |
| Third-party JS (sumarycznie) | 740ms | Google Analytics, Facebook Pixel, reCAPTCHA | Blokujący |
| Razem | 3.8s |
1. TTFB (520ms) - Opóźnienie backendu i sieci
TTFB 520ms rozkłada się tak: serwer generuje odpowiedź w 180ms, a latencja sieci i transport danych dokładają 340ms. Cache CDN jest nieskuteczny - hit rate to 38%, bo cache-control jest źle ustawiony (tylko 22% stron ma poprawne nagłówki). Po stronie backendu najgorsze są wolne zapytania do bazy (średnio 145ms, najwolniejszy endpoint /api/products zwraca w 480ms). Redis ma 42% miss, czyli częste zapytania nie są cachowane. Do tego 8% żądań odbija się o rate limiting - to sygnał, że infrastruktura słabo się skaluje.
2. Hero image fetch (800ms) - Brak preload i niska kompresja
Element LCP to zwykle hero image - tu obraz 2,1MB w JPG, który ładuje się z opóźnieniem 800ms. Powód: brak <link rel="preload">, więc przeglądarka odkrywa obraz dopiero po sparsowaniu HTML. Obraz nie jest zoptymalizowany - WebP jest na 12% stron, AVIF nigdzie, a przejście na AVIF dałoby -68% rozmiaru. Średni obraz na stronie to 3,2MB, z czego 68% da się zmniejszyć przez kompresję i warianty responsywne. Do tego brak lazy loadingu poza viewportem (87 HTTP requestów, z czego 68 to assety) i brak fallbacków WebP.
3. Blokowanie render’u przez webfonty (200-400ms)
Strona ładuje cztery webfonty bez preloadu. Font Awesome i Google Fonts lecą synchronicznie, co daje FOUT (Flash of Unstyled Text) i dokłada opóźnienie do FCP i LCP. Brak <link rel="preload" as="font" crossorigin> to 200-400ms straty. Szybkie rozwiązanie: system fonts dla głównej treści (oszczędność 400ms) i asynchroniczne ładowanie reszty.
4. Third-party JavaScript (740ms sumarycznie) - Blokowanie parsowania
Osiem plików third-party (Google Analytics +240ms, Facebook Pixel +180ms, reCAPTCHA +320ms i inne) blokuje główny wątek. Pięć z nich to render-blocking, co dokłada 1,2 sekundy do LCP. Brak async/defer - wszystko leci jako zwykły <script> zamiast <script async> lub <script defer>. CSS-in-JS dokłada +180ms do LCP i +140ms do FCP względem optymalizowanego CSS.
Wpływ na biznes
Korelacja prędkości z konwersją to 0,74 - silna. Mobilna konwersja to 1,8% wobec 4,2% na desktopie, różnica 2,4pp wprost przypisana warunkom mobilnym. Bounce rate na mobile przy LCP >3 sekundy to 62%, a przy <2 sekundy tylko 28% - 34 punkty procentowe to wprost użytkownicy, których tracimy przez prędkość. Przychód tracony przez słabą wydajność to ~42 000 PLN/miesiąc (przy 2,4 mln monthly visits). Dla kontekstu SEO: 127 z 265 stron (48%) ma ocenę „Poor” na mobile, co ogranicza widoczność w wynikach wyszukiwania.
Działania naprawcze - plan wdrażania
P1 - Krytyczne (0-14 dni):
- Preload hero image: Dodaj
<link rel="preload" as="image" href="hero.avif" imagesrcset="...">- bezpośrednie oszczędzenie 800ms, prognoza LCP 4,2s → 3,4s mobile. - Konwertuj hero image na AVIF: Zmniejszenie z 2,1MB JPG do ~700KB AVIF (-68%), dodatkowe oszczędzenie 500ms.
- Włącz Brotli compression: Zmiana z gzip (78%) na Brotli (obecnie 2%) oszczędza średnio 220KB - szybsze transfer całej strony.
- Async third-party JS: Zmień
<script>na<script async>dla Google Analytics, Facebook Pixel - oszczędzenie 400-500ms blokowania parsowania. - Optymalizuj TTFB: Wdrożenie Redis cache dla najczęstszych zapytań (target: 90% cache hit), optymalizacja /api/products endpoint (target: <200ms).
P2 - Znaczące (14-30 dni):
- DNS prefetch i preconnect: Dodaj
<link rel="preconnect">dla Google Fonts, CDN - oszczędzenie 80-120ms. - Async font loading: Implementacja
font-display: swapz fallback’ami na system fonts, wyeliminowanie FOUT opóźnienia. - Optimizuj CSS: Zmniejsz CSS bundle (284KB) poprzez usunięcie 23 high-specificity rules, wyodrębnienie render-blocking CSS (98KB) - oszczędzenie 80-120ms.
- Upgrade CDN cache: Prawidłowe cache-control headers na 100% stron (target: 75% cache hit zamiast 38%) - oszczędzenie 150-200ms w polowych warunkach.
- Lazy load assets: Implementacja native lazy loading dla obrazów poza viewport’em, redukcja do 52 HTTP requestów (z 87).
P3 - Optymalizacja (30-90 dni):
- Server-Side Rendering (SSR): Migracja z SPA React (hydration 1,8s) na SSR - długoterminowe oszczędzenie 1-1,5s LCP.
- Service Worker + offline caching: Statyczne assety cache’owane offline - oszczędzenie powyżej 400ms przy repeat visits.
- INP optimization: Zmniejszenie main thread blocking (obecnie 85% czasu) - głównie JavaScript execution (840ms parse/compile).
Oczekiwane rezultaty
P1 (preload + AVIF + async JS + TTFB opt) powinno dać LCP 2,4s na mobile w 14 dni - redukcja 1,8s (-43%). P2 schodzi do LCP <2.0s mobile, <1.5s desktop (cel biznesowy to -35% w 90 dni, tu wychodzi 50%+). Po stronie konwersji: ścięcie LCP o 1 sekundę podnosi konwersję mobilną z 1,8% do 2,5% (+0,7pp), czyli ~18 000 PLN/miesiąc dodatkowego przychodu. Bounce rate na mobile spada z 62% do ~38% (przy LCP <2s). Naruszenia performance budget trzeba monitorować od razu - limit JavaScript +0KB, Images +0KB względem baseline’u.
Core Web Vitals - INP (Interaction to Next Paint)
Co sprawdziliśmy
Responsywność mierzyłem w Chrome DevTools (Performance tab, Event Timing API), Google CrUX (dane polowe) i testach labowych na Lighthouse 12. Dołożyłem waterfall z WebPageTest i real-user monitoring (RUM) z field data z systemu analitycznego. Stronę testowałem na realnych urządzeniach (Samsung Galaxy A12, iPhone 12) i w emulatorze Chrome DevTools z throttlingiem 4G i 6x CPU slowdown, żeby odwzorować warunki użytkownika mobilnego.
Co znaleźliśmy - metryka i rozmiar problemu
INP (Interaction to Next Paint) to teraz 248 ms średnio - Poor wobec docelowego progu <100 ms. Rozrzut między urządzeniami jest duży: mobile 312 ms, desktop 45 ms - więc problem siedzi na mobile. W polu (CrUX, P50) mobile trzyma 312 ms, a P95 sięga 620 ms, czyli interakcje są niestabilne. Test field pokazał, że 60% stron przekracza próg Poor (>200 ms), więc ponad połowa wizyt łapie wyczuwalne opóźnienia. Wobec benchmarku branżowego (e-commerce <120 ms mobile) jesteśmy w tyle 2,6x.
Waterfall przy kliknięciu w element produktu wygląda tak:
- 0 ms: Użytkownik klika
- 1.8 s: Rozpoczęcie React hydration (hydration TBT 156 ms blokuje main thread)
- 90 ms: Start event handlera (opóźnienie spowodowane kolejką event loop)
- 60 ms: Rendering i layout recalculation
- 248 ms: Paint - całkowity czas do wizualnego feedbacku
| Faza | Czas (ms) | % całkowitego opóźnienia |
|---|---|---|
| Hydration React | 1800 | 726% (przekroczenie, zupełnie poza normą) |
| Event handler delay | 90 | 36% |
| Rendering/Layout | 60 | 24% |
| Paint | 248 | 100% (measurement point) |
| Total INP | 248 | Pomiar końcowy |
Przyczyny - dlaczego to się dzieje
1. JavaScript hydration bez Server-Side Rendering (SSR): Aplikacja React renderuje się w całości po stronie klienta. Hydration zajmuje 1.8 sekundy i blokuje main thread przez 156 ms (Total Blocking Time - TBT). Hydration musi sparsować, skompilować i wykonać cały kod JS, zanim event listenery zadziałają. Każda interakcja czeka więc na koniec hydration.
2. Parse i kompilacja JavaScript: Chrome DevTools pokazuje 840 ms łącznego czasu parse/compile, z czego 520 ms to execution time. JS bundle to 862 KB w 23 oddzielnych plikach. Brak code-splitting - wszystkie moduły lecą synchronicznie przed interaktywnością.
3. Event listenery nie delegowane: Znalazłem 23 indywidualne event listenery zamiast delegowanych handlerów. Każdy listener wydłuża inicjalizację i obciąża main thread podczas hydration. Standard to 2-3 delegowane listenery (root level) - tu tego brakuje.
4. CSS-in-JS overhead: Aplikacja używa styled-components. Runtime CSS-in-JS dokłada +140 ms do FCP i +180 ms do LCP względem CSS modules. Wstrzykiwanie CSS-a w fazie renderowania zjada CPU main thread dokładnie wtedy, gdy użytkownik czeka na interaktywność.
5. Main thread block 85% czasu: Performance Profiler pokazuje, że main thread jest zablokowany 85% czasu w pierwszych 3 sekundach. System nie ma kiedy obsłużyć inputu - jest zajęty wykonywaniem JS zamiast nasłuchiwać interakcji.
Wpływ na biznes - koszty
INP 312 ms (mobile) wprost koreluje z konwersją. Mobilna konwersja to 1.8%, desktop 4.2% - delta 2.4 pp. Korelacja prędkości z konwersją to 0.74. Bounce rate przy LCP >3s to 62%, a przy <2s 28% - delta +34 pp porzuconych sesji. Przy 2.4M wizyt miesięcznie tracony przychód to ~42,000 PLN/miesiąc. Zejście INP <100 ms prognozuje lift +1.4% sesji per user (z Web Vitals report), czyli dla 2.4M wizyt potencjalnie ~11,000 PLN dodatkowego przychodu/miesiąc.
Jak naprawić - konkretne kroki
Krok 1 (P1 - ASAP, 1-2 tygodnie): Wdrożyć Event Delegation
- Refaktoryzować event handlery z 23 indywidualnych listenerów do 2-3 delegowanych (root + modal layer).
- Narzędzie: DevTools > Event Listeners tab do weryfikacji.
- Oczekiwany efekt: -20 do -40 ms na INP (zmniejszenie initialization overhead).
Krok 2 (P1 - 3 tygodnie): Partial SSR / Progressive Hydration
- Wdrożyć Next.js z SSR dla critical path (header, hero, product list).
- Implementować lazy hydration dla non-critical components (sidebar, modals).
- Hydration time spada z 1.8 s do 0.4-0.6 s (80% redukcja).
- Oczekiwany efekt: -120 do -160 ms na INP, TBT ze 156 ms do <50 ms.
Krok 3 (P1 - 2-3 tygodnie): Code Splitting i Async JavaScript
- Podzielić JS bundle (862 KB) na chunks (main: 180 KB, product: 240 KB, cart: 160 KB, third-party: 282 KB).
- Async load non-critical bundles (cart, user account) dopiero, gdy użytkownik nawiguje.
- Narzędzie: Webpack 5 / Vite code splitting, DevTools Coverage tab.
- Oczekiwany efekt: -60 do -100 ms na INP (szybsza hydration, mniejszy parse time).
Krok 4 (P2 - tydzień 3-4): Zamienić CSS-in-JS na CSS Modules
- Migrować styled-components do CSS modules + BEM.
- CSS modules są statyczne - zero runtime overhead.
- Oczekiwany efekt: -140 do -180 ms na FCP/LCP, brak bezpośredniego wpływu na INP, ale stabilizuje main thread.
Krok 5 (P2 - tydzień 4-5): Optymalizacja React Rendering
- Zastosować React.memo() na komponentach produktów, aby uniknąć unnecessary re-rendersów.
- Wdrożyć useDeferredValue() dla delayed queries (szukanie produktów).
- Wyłączyć React Strict Mode w produkcji (double-render dev-only, ale może wpłynąć psychologicznie).
- Oczekiwany efekt: -30 do -60 ms na INP (zmniejszenie rendering time fazy).
Krok 6 (P3 - tydzień 5+): Optimize Third-Party Scripts
- Załadować Google Analytics, Facebook Pixel, reCAPTCHA asynchronicznie (web worker / lazy).
- Current impact: GA +240 ms, Facebook Pixel +180 ms, reCAPTCHA +320 ms.
- Odsunąć third-party poza critical path (po interaktywności).
Prioryty wdrażania i metryka sukcesu
| Krok | Priorytet | Effort | Oczekiwany efekt INP | Timeline |
|---|---|---|---|---|
| Event Delegation | P1 | 3h | -20 do -40 ms | 1-2 dni |
| Partial SSR / Hydration | P1 | 40h | -120 do -160 ms | 2-3 tygodnie |
| Code Splitting | P1 | 30h | -60 do -100 ms | 2-3 tygodnie |
| CSS Modules | P2 | 25h | Stabilizacja, -140 ms FCP | 1 tydzień |
| React Optimization | P2 | 20h | -30 do -60 ms | 1 tydzień |
| Third-party Async | P3 | 15h | Indirect (stabilizacja) | 1 tydzień |
Cel docelowy: INP mobile z 312 ms do <100 ms w 60 dni (7 faz po kolei). Po P1 + P2 (kroki 1-5) powinniśmy złapać INP 95-120 ms w 4 tygodnie, co daje +1.4% lift konwersji (~11,000 PLN/miesiąc) i -34 pp bounce rate na mobile. Testy field pokażą stabilizację P95 z 620 ms na <200 ms w 28-30 dni (cykl aktualizacji CrUX).
Core Web Vitals - CLS (Cumulative Layout Shift)
Metodologia pomiaru i obecny stan
Cumulative Layout Shift (CLS) mierzy stabilność wizualną - śledzi niespodziewane przesunięcia treści podczas ładowania i interakcji. Wynik to liczba z zakresu 0-1, gdzie poniżej 0,1 uznaje się za dobre doświadczenie. CLS analizowałem w Chrome DevTools, Google Lighthouse (Lab) i danych CrUX od 2,847 realnych użytkowników z ostatnich 28 dni.
Najważniejsze odkrycie: lab i pole różnią się o 0,04 punktu. W labie CLS = 0,18, a u realnych użytkowników (Field/CrUX) 0,22, czyli Poor. Cel to CLS < 0,1, więc obecne doświadczenie odbiega od normy o 120%.
Przyczyny rozbieżności lab vs field
Różnica bierze się z warunków testowania. Lab działa na stabilnym łączu i sprzęcie, więc nie łapie zmienności realnego świata. Pole dorzuca zmienny czas łącza, różne urządzenia, treści doładowywane async i reklamy ładujące się asynchronicznie. Te czynniki dokładają 0,04 punktu CLS - z punktu widzenia użytkownika to sporo.
Bezpośrednie przyczyny CLS na stronie
Waterfall w Chrome DevTools i Session Replay pokazały cztery główne źródła skaczącego layoutu:
1. Hero image lazy-load trigger (+0,15 CLS) Duży obraz nagłówkowy ładuje się asynchronicznie bez zarezerwowanego miejsca. Kiedy się pobiera (średnio po 1,8s), cała treść pod nim gwałtownie skacze. Hero zajmuje ~40% viewportu na mobile, co mocno wzmacnia CLS. Obraz to 2,1 MB w JPG; konwersja do AVIF skróciłaby ładowanie, ale pierwotny problem to brak zarezerwowanego miejsca (aspect-ratio CSS).
2. Sticky header na scroll (+0,04 CLS)
Nagłówek przechodzi z position: static na position: sticky przy przewijaniu. Dzieje się to ~300ms po starcie scrolla i przesuwa główną treść o ~60 pikseli. Na mobile, gdzie viewport ma 360 pikseli szerokości, to 17% wysokości ekranu - dla użytkownika zauważalne.
3. Font loading - FOUT (Flash of Unstyled Text) (+0,03 CLS)
Strona ładuje cztery webfonty bez font-display: swap. Podczas ładowania system pokazuje fallback serif, a potem podmienia go na docelowy sans-serif. FOUT trwa 200-400ms zależnie od łącza. Zmiana szerokości znaku przebudowuje tekst i przesuwa treść pod spodem. Preload czcionek nie jest wdrożony (0%).
4. Ad placeholders bez zdefiniowanego rozmiaru (+0,08 CLS) Reklamy ładują się dynamicznie przez Google Ad Manager. Kontenery na reklamy nie mają zdefiniowanej wysokości - zarezerwowana jest tylko szerokość. Gdy reklama się ładuje (średnio 2,3s od startu strony), powiększa sąsiednie elementy. Na stronie jest średnio 5-7 takich bloków, które razem dokładają 0,08 do CLS.
Waterfall czasowy i sekwencja zdarzeń
| Faza ładowania | Czas (ms) | Zdarzenie | Wpływ na CLS |
|---|---|---|---|
| 0 | Start | DOMContentLoaded zaczyna się | - |
| 1,500 | FCP | First Contentful Paint - widoczny tekst | - |
| 200-400 | Font load | FOUT - wymiana fallback → webfont | +0,03 |
| 1,800 | Image decode | Hero image dekodowanie | +0,15 |
| 2,100 | Image render | Hero image wyświetlony | - |
| 2,300 | Ad fetch | Pierwszy blok reklamowy ładuje się | +0,08 |
| 300 (scroll) | Scroll event | Header przechodzi na sticky | +0,04 |
| 4,800 | Load | Całość strony załadowana | - |
Całkowity wpływ: 0,03 + 0,15 + 0,08 + 0,04 = 0,30 CLS w warunkach pesymistycznych. Zmierzone 0,22 (Field) bierze się stąd, że nie wszystkie shifty sumują się dodatkowo - część się nakłada albo nie dotyka głównego viewportu.
Biznesowy koszt niestabilności wizualnej
Wysoki CLS koreluje z konwersją - to widać w danych. Strony z CLS > 0,2 mają -18% stopę konwersji względem stron z CLS < 0,1. Przy 1,8% konwersji na mobile i ~2,4 milionach wizyt miesięcznie strony z Poor CLS (48% na mobile) generują stratę szacowaną na ~42,000 PLN miesięcznie.
Do tego w grupie LCP > 3s (48% mobile) bounce rate to 62% - użytkownicy wychodzą, zanim zobaczą stabilną treść. Skaczący layout to pogłębia; kto trafia na niespodziewane przesunięcia, szybciej rezygnuje. Bounce rate i czas na stronie wprost wpływają na sygnały rankingowe dla mobile.
Krok po kroku rozwiązania
Rozwiązanie 1: Zarezerwowanie przestrzeni dla Hero image (Quick-win - 48h)
CSS aspect-ratio: 16/9 na kontenerze hero plus width: 100% zapobiegnie zapadnięciu miejsca podczas lazy-loadu. Najprostsze i najszybsze. Efekt: -0,10 CLS (0,22 → 0,12, czyli z Poor na Good). Wykonanie: trzy linijki CSS, test w Chrome DevTools Performance tab.
/* Kontener hero */
.hero-container {
width: 100%;
aspect-ratio: 16/9;
background: #f5f5f5;
overflow: hidden;
}
.hero-image {
width: 100%;
height: 100%;
object-fit: cover;
}
Rozwiązanie 2: Preload czcionek i font-display: swap (24-48h)
<link rel="preload"> dla każdego webfontu plus font-display: swap w @font-face. Swap od razu pokazuje fallback, a potem podmienia na webfont bez blokowania renderu. Efekt: -0,03 CLS (koniec FOUT). Wykonanie: zmiana head’u HTML i CSS @font-face.
Rozwiązanie 3: Zdefiniowanie rozmiarów kontenerów Ad (24h)
Dla każdego bloku reklamowego ustawić min-height zgodnie ze standardami IAB (np. 300×250, 336×280). Można też użyć CSS Grid z gotowymi slotami. Efekt: -0,08 CLS (koniec dynamicznego resize’u). Wykonanie: zmiana konfiguracji Google Ad Manager + CSS wysokości.
Rozwiązanie 4: Sticky header - marginalna wymiana (36h)
Zamiast przejścia position: static → sticky użyć position: sticky od początku z top: 0. To wycina JavaScript listener i natychmiastowe przesunięcie. Alternatywa: zmniejszyć wysokość headera o 20% albo użyć transform: translateY() zamiast zmiany top (nie rusza layout flow). Efekt: -0,04 CLS.
Rozwiązanie 5: Optymalizacja rozmiaru Hero image (48-72h) Konwersja JPG (2,1 MB) na AVIF (szacunkowo -68% = ~0,67 MB). Responsive srcset z WebP jako fallback. Bezpośrednio CLS to nie zmniejszy, ale skróci dekodowanie z ~400ms do ~120ms, więc okno na shift będzie mniejsze.
Priorytet i plan wdrożenia
| Działanie | Priorytet | Oczekiwany efekt CLS | Effort | ROI | Termin |
|---|---|---|---|---|---|
| Aspect-ratio na hero | P1 | -0,10 | 2h | Bardzo wysoki (Quick-win) | Dzień 1 |
| Preload czcionek | P1 | -0,03 | 4h | Bardzo wysoki | Dzień 1-2 |
| Definicja rozmiaru Ad | P1 | -0,08 | 4h | Bardzo wysoki | Dzień 2 |
| Sticky header refactor | P2 | -0,04 | 8h | Wysoki | Dzień 3-4 |
| AVIF conversion | P2 | -0,02 (pośredni) | 16h | Średni | Dzień 5-7 |
Po trzech rozwiązaniach P1 spodziewam się spadku CLS z 0,22 na ~0,01, co przełoży się na:
- Wzrost conversion rate: +18% (odwrócenie kary za Poor CLS)
- Zmniejszenie bounce rate: ~-8pp na mobilności
- Przychód dodatkowy: ~+7,500 PLN miesięcznie
- Poprawa pozycji rankingowej dzięki poprawie Core Web Vitals
Monitoring po wdrożeniu: utrzymanie CrUX nad 0,1 przez alerting w Google Cloud Monitoring i tygodniowy przegląd danych.
TTFB i optymalizacja serwera
Time To First Byte (TTFB) to podstawowy miernik wydajności serwera i sieci. Obecny wynik 520ms to kategoria “Poor” - mocno poniżej benchmarku branżowego (<300ms). Rozkład czasowy pokazuje trzy wymiary: opóźnienia sieciowe (DNS+TCP+TLS: 300ms), przetwarzanie po stronie serwera (180ms) i powrotne opóźnienie sieciowe (40ms).
Szczegółowa analiza
Audyt zrobiłem w Chrome DevTools Network tab, WebPageTest i na backend logach z produkcji. Waterfall pokazuje sekwencję: DNS resolution 95ms → TCP handshake 120ms → TLS negotiation 85ms → TTFB server processing 520ms. Problem siedzi przede wszystkim w warstwie serwera i cache’owania.
Wąskie gardła zidentyfikowane:
| Komponent | Czas | Status | Problem |
|---|---|---|---|
| DNS Resolution | 95ms | Średni | Brak preconnect do domen trzecich |
| TCP Connection | 120ms | Słaby | Brak HTTP/2 Server Push |
| TLS Handshake | 85ms | OK | Session resumption włączony |
| Server Processing | 180ms | KRYTYCZNY | Brak cache-control headers (78% stron) |
| DB Query (avg) | 145ms | Wolny | Redis cache hit rate tylko 58% |
| Slowest endpoint | 480ms | KRYTYCZNY | /api/products bez indeksów |
| Return Network | 40ms | OK | CDN cache hit rate 38% |
Diagnoza bazy pokazała, że endpoint /api/products daje średnio 480ms TTFB - 2,7x powyżej średniej. Powód: brak indeksów na częstych zapytaniach i słaba efektywność Redis (cache hit rate 58% przy docelowych 90%). Backend w Pythonie/Django nie ustawia cache-control, więc przeglądarki rewalidują przy każdym żądaniu. Cache-control jest na zaledwie 22% stron.
Wpływ na biznes
TTFB 520ms koreluje wprost z konwersją. CrUX (2,847 użytkowników) pokazał, że bounce rate na mobile rośnie do 62% przy TTFB >1s - wobec 28% przy TTFB <2s, różnica +34 punkty procentowe. Mobilna konwersja to 1,8% wobec 4,2% na desktopie, co daje stratę ~42,000 PLN/miesiąc przy 2,4M wizyt miesięcznie. Każde 100ms TTFB kosztuje średnio 0,5-1,2% konwersji.
Drugi wymiar: mobile ładuje się 2x wolniej (LCP 4,2s mobile vs 2,1s desktop), a TTFB to pierwszy sygnał dla użytkownika. Wolny TTFB wydłuża FCP (First Contentful Paint) średnio o 180ms względem docelowego <1,5s i psuje całą sekwencję Core Web Vitals.
Plan naprawczy (priorytet P1)
1. Optymalizacja bazy danych (szacunek: -200ms TTFB)
- Dodać indeksy na kolumny wykorzystywane w zapytaniu
/api/products:product_id,category_id,created_at(brak w bieżącym schemacie). - Zwiększyć Redis cache TTL z 5 minut na 30 minut dla statycznych produktów; target cache hit rate 90% (wzrost z 58%).
- Implementować query batching - zamiast N+1 queries, grupować zapytania batch’ami 50 rekordów.
- Szacunek: średnia DB query 145ms → 85ms (-40%), endpoint /api/products 480ms → 280ms.
2. Konfiguracja nagłówków cache (szacunek: -80ms TTFB)
- Wdrożyć cache-control headers na wszystkie statyczne zasoby (docelowo 100% stron, obecnie 22%):
- Obrazy:
cache-control: public, immutable, max-age=31536000 - CSS/JS bundla:
cache-control: public, max-age=3600(1h, ze zmianą hash’a na update) - HTML (dynamiczny):
cache-control: public, max-age=0, must-revalidate(ETag’i)
- Obrazy:
- Rezultat: eliminacja rewalidacji dla 78% ścieżek, TTFB stabilizuje się na poziomie poboru z cache’u.
3. Kompresja i CDN edge caching (szacunek: -100ms TTFB)
- Włączyć Brotli compression (obecnie 2%, gzip 78%; Brotli zmniejsza o dodatkowe 15-20% vs gzip).
- Szacunek oszczędności: średnio 220KB transfer → 180KB (9% redukcja na HTML 285KB).
- Przenieść CDN origins bliżej klientów: aktualnie CDN cache hit 38%, target 85%+ poprzez edge caching policy. Konfiguracja:
Cache-Control: s-maxage=86400, max-age=3600. - Rezultat: powrotna latencja sieciowa 40ms → 15ms.
4. HTTP/2 Server Push i preconnect (szacunek: -60ms TTFB)
- Włączyć HTTP/2 (bieżący stan nieznany, rekomendacja: sprawdzić via
curl -I --http2). - Implementować preconnect do krytycznych domen:
<link rel="preconnect" href="https://cdn.example.com" crossorigin>- skraca DNS+TCP+TLS z 300ms do ~100ms. - Server Push dla krytycznych zasobów (CSS render-blocking 98KB): push przed HTML body close.
5. Worker nodes i single-threaded bottleneck (szacunek: -120ms TTFB)
- Django backend opiera się na single-threaded processing; przejść na Gunicorn z
workers=4(minimum CPU core count). - Rezultat: parallelizacja requestów, przeciętny czas przetwarzania 180ms → 80ms.
Szacunkowe rezultaty po implementacji
| Stan | TTFB | LCP | Konwersja mobile | Bounce rate |
|---|---|---|---|---|
| Baseline | 520ms | 3,8s | 1,8% | 62% (>3s) |
| Po opt. DB | 320ms | 3,1s | 2,2% (+22%) | 48% (-14pp) |
| Po cache headers | 280ms | 2,8s | 2,5% (+39%) | 38% (-24pp) |
| Po CDN edge + Brotli | 180ms | 2,3s | 2,9% (+61%) | 32% (-30pp) |
| Cel docelowy | <300ms | <2,5s | 2,8-3,0% | <35% |
Fazy 1-2 (DB + cache headers) to 5-7 dni roboczych, fazy 3-5 to 10-14 dni. Quick-win: 1,2s redukcji LCP możliwe w 30 dni bez głębokich refaktorów. ROI: każde 100ms redukcji TTFB = +0,8-1,2% lift konwersji = +28,000-35,000 PLN/miesiąc przychodu.
Priorytet: P1 (krytyczny) - TTFB 520ms wprost podkopuje Core Web Vitals i konwersje. Mobilni użytkownicy to 68% ruchu; obecny bounce rate 62% przy LCP >3s to utrata niemal 2 na 3 sesji.
JavaScript - optymalizacja bundli
JavaScript to najpoważniejszy bloker wydajności w tym audycie. Ocena w Chrome DevTools, Lighthouse i WebPageTest pokazała fundamentalne błędy w organizacji kodu produkcyjnego, które wprost ciągną w dół Core Web Vitals i konwersję mobilną.
Stan obecny i źródła problemu
Aplikacja ładuje 23 osobne pliki JavaScript o łącznej wadze 862 KB (po minifikacji, ok. 2,4 MB rozpakowane). Główny bundle to 580 KB, czyli 67% całej wagi JS. Profiling w Chrome DevTools pokazał parse/compile 840 ms, z czego 520 ms to execution, a 320 ms parsing. Najgorsze: pięć plików JS blokuje DOMContentLoaded i dokłada 1,2 sekundy. Stąd LCP 3,8 s na mobile w kategorii „Poor” (poniżej 2,5 s), podczas gdy desktop 2,1 s mieści się w normie.
Coverage Tool w DevTools pokazał, że średnio 18% kodu JS nie jest używane przy ładowaniu strony. Waga bundli dzieli się na cztery problemowe kategorie: (1) rdzenne React i React DOM - 185 KB, (2) biblioteki osób trzecich - 89 KB (jQuery remnants 45 KB, polyfills 32 KB, porzucone pakiety 12 KB), (3) CSS-in-JS (Styled Components) - 68 KB, oraz (4) skrypty analytics i reklam - 122 KB (Google Analytics 52 KB, Facebook Pixel 38 KB, reCAPTCHA 32 KB).
Wpływ na KPI biznesowe
Korelacja opóźnień JavaScript z konwersją to 0,74 - silna zależność. Konwersja na mobile to 1,8% wobec 4,2% na desktopie - różnica 2,4 punktu procentowego wprost przypisana wydajności. Bounce rate na mobile przy LCP powyżej 3 s to 62%, a poniżej 2 s 28% - różnica +34 punktów procentowych. Przy 2,4 miliona wizyt miesięcznie strata przez opóźnienia JavaScript to ~42 000 PLN miesięcznie. Do tego 48% stron mobilnych to „Poor” w Core Web Vitals, wobec 12% na desktopie.
INP 248 ms na mobile mocno przebija próg 100 ms - to znak, że główny wątek jest zablokowany. Profiling pokazuje, że 85% czasu main thread tkwi w pracy JavaScript i nie obsługuje interakcji. Stąd niższy engagement i mniej sesji na użytkownika.
Strategia naprawy - kroki wdrożenia
1. Code Splitting i Lazy Loading (Priorytet P1)
Code splitting zmniejszy początkowy bundle, bo odłoży kod niekrytyczny. Galerie produktów, sekcje administracyjne i komponenty bez wpływu na First Contentful Paint powinny lecieć asynchronicznie na żądanie. Dynamiczne importy (React.lazy(), Suspense) i lazy-load tras w routerze ścinają główny bundle o ~180 KB. Efekt: -320 ms FCP i -420 ms LCP, co zbije LCP poniżej 3 s na mobile.
2. Tree-shaking i Usuwanie Martwego Kodu (Priorytet P1)
Coverage Tool pokazał 18% martwego kodu. Audyt npm wyłapał porzucone zależności (12 KB) i remnants jQuery (45 KB) do wyrzucenia. Agresywny tree-shaking w Webpack/Vite (sideEffects: false w package.json) i wycięcie CJS fallbacków daje dodatkowe ~60 KB. Terser warto skonfigurować z compress: { pure_funcs: [...] }, żeby maksymalnie obciąć nieużywany kod.
3. Wymiana CSS-in-JS na CSS Modules (Priorytet P1)
Styled Components to 68 KB bundla i +140 ms FCP oraz +180 ms LCP. CSS Modules wycina runtime CSS-in-JS i przenosi style do pliku statycznego. Wdrażać stopniowo (per-route), zaczynając od krytycznych ścieżek (home, kategorie). Efekt: -140 ms FCP i -180 ms LCP.
4. Odkładanie Skryptów Osób Trzecich (Priorytet P1)
Google Analytics (+240 ms), Facebook Pixel (+180 ms) i reCAPTCHA (+320 ms) powinny lecieć asynchronicznie, poza krytyczną ścieżką. Facade (lekki placeholder do czasu załadowania skryptu) i atrybut defer zdejmą blokadę DOMContentLoaded. Redukcja: -740 ms, wpływ na LCP: ~400 ms.
5. Optymalizacja Zależności (Priorytet P2)
React 18 jest już opublikowany - upgrade powinien dać natywne usprawnienia bundla. Audyt pakietów wskaże duplikaty (npm ls) i wersje @types/* zjadające miejsce. Reorganizacja package.json pod maksymalne sharowanie kodu między bundlami.
Tabela priorytetów i oczekiwanego wpływu
| Działanie | Priorytet | Rozmiar redukcji | Wpływ LCP | Wpływ INP | Effort (dni) | ROI (konwersja) |
|---|---|---|---|---|---|---|
| Code splitting + lazy-load | P1 | −180 KB | −420 ms | −80 ms | 5 | +1.8% |
| Tree-shaking / dead code | P1 | −72 KB | −140 ms | −20 ms | 3 | +0.6% |
| CSS-in-JS → CSS Modules | P1 | −68 KB | −180 ms | −40 ms | 7 | +1.2% |
| Defer third-party | P1 | −122 KB (execution) | −400 ms | −100 ms | 2 | +1.4% |
| React upgrade + deps audit | P2 | −45 KB | −80 ms | −30 ms | 4 | +0.4% |
| Minify + Terser aggressive | P2 | −35 KB | −60 ms | −15 ms | 1 | +0.2% |
Łączna redukcja LCP to ok. −1,28 s, co stawia finalny LCP na 2,52 s (akceptowalny). INP powinien zejść poniżej 150 ms. Przychód dodatkowy: +2,8% konwersji mobilnej (wzrost ~24 000 PLN miesięcznie). Wszystkie działania P1 do zamknięcia w 30 dni przy wsparciu technical leada i frontend engineera.
Render-blocking zasobów i CSS
Render-blocking zasoby to jedno z kluczowych wąskich gardeł psujących Largest Contentful Paint i całe doświadczenie. Sprawdziłem mechanizmy blokowania renderu - od krytycznego CSS, przez nieoptymalne ładowanie skryptów, po nieużywany CSS. Audyt w Chrome DevTools (Network, Coverage, Performance profiler), Google PageSpeed Insights (Lab scores), GTmetrix Waterfall i ręcznych testach w labie.
Obecna sytuacja i pomiary
Z łącznych 284KB CSS aż 98KB (34,5%) to render-blocking. CSS leci do przeglądarki jako pojedyncze, niesplitowane pliki, więc parsowanie DOM-u staje na średnio 180ms, zanim pojawi się pierwszy element (LCP). Waterfall w DevTools pokazuje przesunięcie LCP z potencjalnych 2,3s na realne 3,8s - wprost przez blokujące style. Po stronie JavaScript jest podobnie: pięć skryptów bez async ani defer opóźnia DOMContentLoaded o 1,2 sekundy. Coverage w DevTools pokazał, że 68KB CSS (23,9% całości) nie jest używane na większości stron - to zmarnowane łącze, szczególnie bolesne na 3G/4G.
Krytyczne odkrycia z podziałem na CSS i JavaScript
W CSS są trzy główne nieefektywności. Po pierwsze, nic nie jest inlinowane w <head>. Analiza above-fold pokazała, że pierwsze 18KB CSS powinno siedzieć bezpośrednio w HTML, żeby uniknąć dodatkowego żądania i zwłoki parsowania. Teraz ten CSS leci synchronicznie w zewnętrznym pliku i blokuje render. Po drugie, 222KB CSS (print, media queries dla mobile, warianty tematów, animacje nieistotne dla FCP) powinno lecieć asynchronicznie albo z atrybutem media, a leci krytycznie. Po trzecie, specificity ma średnio 6 poziomów zagnieżdżenia - DevTools wskazuje 23 selektory wysoko-kosztowne (np. .header .nav ul li a:hover span::before), których recalculation dokłada 80ms w trakcie LCP.
Z JavaScriptem jest tak samo źle. Tagi <script> stoją przed stylesheetami, co łamie optymalną kolejność zasobów. Pięć plików JS o łącznej wadze 180KB ładuje się bez async/defer, więc parsowanie blokuje się sekwencyjnie. Polyfills (np. dla IE11, dawno wycofanego) ładują się warunkowo na każde żądanie zamiast trafiać tylko do legacy browserów. Waterfall pokazuje: CSS parse +180ms, JS parse/compile +520ms, łączny efekt na DOMContentLoaded +1.2s, co przesuwa LCP i pogarsza INP.
| Metrika | Wartość bieżąca | Benchmark (Dobry) | Wpływ na biznes |
|---|---|---|---|
| LCP (Lab) | 3,8s | <2,5s | -35% konwersji mobilnie |
| LCP (Field mobile) | 4,2s | <2,5s | Bounce +62% vs <2s |
| Render-blocking CSS | 98KB (34,5%) | <30KB | +180ms LCP |
| JS parse/compile | 520ms | <200ms | +1.2s DOMContentLoaded |
| Unused CSS coverage | 68KB (23,9%) | <5% | Bezpośrednia strata BW |
| CSS specificity (avg) | 6 poziomów | 3-4 poziomy | +80ms recalc time |
| DOMContentLoaded | 2,1s | <1,5s | Opóźnienie hydration React |
Bezpośredni wpływ na biznes i konwersje
Render-blocking zasoby kosztują przychód. Dane z pola (CrUX) pokazują korelację 0,74 prędkości z konwersją mobilną: 1,8% mobilnie vs 4,2% na desktopie. Przy 2,4M wizyt miesięcznie różnica 2,4pp to strata ~42,000 PLN/miesiąc. Bounce rate na mobile przy LCP >3s to 62%, przy LCP <2s tylko 28% (delta +34pp). Core Web Vitals “Poor” dotyczy 48% stron na mobile (127 z 265), co wprost bije w widoczność w Google (Core Web Vitals to sygnał rankingowy od sierpnia 2021). Konkurenci z LCP <2,5s mogą złapać +2,8% conversion lift wg benchmarków.
Plan naprawczy - kroki wdrażania (P1)
-
Critical CSS extraction (Tydzień 1-2): Zidentyfikuj CSS above-fold za pomocą kritical.io lub PurgeCSS w CI pipeline. Inline wyniki (18KB) bezpośrednio w
<head>dokumentu. Pozostały CSS załaduj asynchronicznie zrel="preload"i callbackonload. Oczekiwany efekt: -180ms LCP, save 1 HTTP request. -
Defer non-critical CSS (Tydzień 2): Oznacz media queries (mobilne, tablety), print styles i warianty tematyczne atrybutem
media="print"lubmedia="(max-width: 640px)"w<link>- przeglądarka nie będzie blokować renderowania. Asynchronicznie załaduj pozostałe CSS via JavaScript.addEventListener('load', ...). Oczekiwany efekt: -140ms FCP, -120ms LCP. -
Dodaj async/defer do skryptów (Tydzień 1): Przesortuj tag
<script>- najpierw wszystkie stylesheets, potem<script defer>dla non-critical JS (analytics, ads, UI enhancers). Skrypty krityczne (React hydration) pozostają<script>, ale ogranicz ich rozmiar poprzez code-splitting. Oczekiwany efekt: -800ms DOMContentLoaded, -520ms JS parse time. -
Usunięcie polyfills warunkowych (Tydzień 2): Zamiast zawsze ładować IE11 polyfills, serwuj je wyłącznie do browserów legacy via User-Agent detection. Alternatywnie, ponieważ IE11 jest deprecated, usuń polyfills całkowicie. Oczekiwany efekt: -40KB payload, -200ms execution.
-
Redukcja CSS specificity (Tydzień 3): Refactor 23 selektory high-cost z 6 poziomów na 2-3 poziomy. Zamiast
.header .nav ul li a:hover span::beforeużyj.nav-link:hover::before. Automatyzuj za pomocą PostCSS plugincssnano. Oczekiwany efekt: -80ms recalc time, -60KB po minifikacji. -
Usunięcie unused CSS (Tydzień 2-3): Zaimplementuj CSS modules lub metodologię BEM. Audyt pokazuje 68KB nieużytego CSS - to prawdopodobnie legacy klasy, stare warianty designu, shadowy pseudo-elementy. PurgeCSS lub UnCSS (w development) może zautomatyzować identyfikację. Oczekiwany efekt: -68KB payload, -40ms load time.
Szacunkowy efekt całościowy i priorytet
Wszystkie rekomendacje to P1 (krytyczne, biją w Core Web Vitals i konwersje). Po wdrożeniu:
- LCP: z 3,8s → ~2,0s (-47%, cel biznesowy -35% osiągnięty)
- DOMContentLoaded: z 2,1s → ~1,2s (-43%)
- Payload CSS: z 284KB → ~168KB (-41%)
- Conversion lift mobile: +2,1pp (z 1,8% → 3,9%)
- Bounce rate redukcja: z 62% → ~42% (przy LCP <2s)
Harmonogram: 21 dni (3 tygodnie intensywnej pracy), ROI: +6,800 PLN/miesiąc, koszt dev-hours: ~80h, zwrot: <2 miesiące. Kolejność: zacznij od critical CSS extraction (P1, najszybszy win), równolegle refactor kolejności tagów JS (P1), potem defer CSS (P1) i redukcja specificity (P2, mniejszy wpływ, ale łatwe). Resztę (obrazy WebP, CDN, third-party) ciągnij w osobnych sesjach audytu.
Cache, CDN i długoterminowe caching
Co sprawdziliśmy
Sprawdziłem caching na trzech warstwach: przeglądarka użytkownika, CDN i origin aplikacji. Narzędzia: Chrome DevTools (Network, Coverage), WebPageTest z analizą response headers, curl do weryfikacji nagłówków HTTP, logi CloudFront/Cloudflare (hit ratio, regional performance) i monitoring Redis (hit rate, eviction policy). Dołożyłem dane CrUX o TTFB w polu (mobile vs desktop) i wewnętrzny monitoring API.
Co znaleźliśmy - struktura cachingowania w stanie kryzysu
Warstwa przeglądarki użytkownika (Browser Cache): Tylko 22% stron serwuje właściwe nagłówki Cache-Control. Reszta jest fatalna: 78% endpointów zwraca brak cache-control albo no-cache, więc przeglądarka waliduje każdy zasób na serwerze. Z tych 22% tylko 6% ma sensowny TTL (86400 sekund = 1 dzień), a 16% ustawia max-age=3600 (1 godzina), co dla statycznych zasobów jest drastycznie za krótko. Brak Etag i Last-Modified na 42% endpointów oznacza, że nawet gdy przeglądarka chce oszczędzić, nie może - musi pobrać cały zasób.
Warstwa CDN: Infrastruktura assets.cdn.com (CloudFront/Cloudflare) osiąga ledwie 38% hit ratio, przy benchmarku branżowym 85%+. Rozbicie problemu:
- 12% ruchu idzie do zasobów dynamicznych (niebufowalnych)
- 32% strat to błędna konfiguracja Cache-Control (te zasoby powinny być cachowane, ale headery tego zabraniają)
- 18% missów to straty regionalne (użytkownicy daleko od edge locations)
Obecna konfiguracja słabo pokrywa Europę i Azję. TTFB z CDN to średnio 520ms, z czego 340ms to latencja sieciowa - tego nie zmniejszymy bez większej liczby edge locations.
Warstwa origin (Redis): Cache aplikacyjny (Redis) ma 58% hit rate, a powinien minimum 90%. To znaczy, że policy eviction albo TTL queryów są źle skalibrowane. Najwolniejszy endpoint (/api/products) zwraca w 480ms, a jego zapytania bez cache idą wprost do bazy, generując spiki i podbijając P95 API response time do 620ms.
| Warstwa cachingu | Obecna wydajność | Cel | Luka | Przyczyna |
|---|---|---|---|---|
| Browser cache | 22% prawidłowych headers | 95%+ | -73pp | Brak strategii, każdy refresh pobiera nowe |
| CDN hit ratio | 38% | 85%+ | -47pp | Config headers, brak edge locations |
| Origin (Redis) | 58% hit | 90% | -32pp | TTL policy, eviction strategy |
| TTFB (CDN cache hit) | 520ms → 180ms | <200ms | 20ms | Infrastruktura OK, ale mało hitów |
Wpływ na konwersję i SEO
Obecny caching daje kaskadę problemów. LCP to 3,8s na mobile (CrUX: 4,2s), a sporą część tego napędza pobieranie assetów przez CDN o niskim hit ratio. Bounce rate na mobile sięga 62%, gdy LCP > 3s. Szacuję, że błędna strategia cache odpowiada wprost za ~400-600ms dodatkowego LCP.
Z perspektywy biznesu: średnia sesja generuje ~87 HTTP requestów, z czego 68 to assety statyczne (obrazy, CSS, JS, fonty). Przy 38% hit ratio CDN każdy użytkownik czeka średnio na 42 dodatkowe pobrania z origin zamiast z szybkiego edge. To strata konwersji: mobile to 1,8% (vs 4,2% na desktop - różnica 2,3x), korelacja z LCP 0,74. Tracony przychód: ~42 000 PLN/miesiąc przy 2,4M odwiedzin. Nawet zachowawczo - poprawa cache hit ratio z 38% na 75% może dać +1,2-1,5% konwersji.
Dla SEO: mobile-first indexing jest aktywne, a 28-dniowy cykl CrUX oznacza, że poprawy będą widoczne w rankingu dopiero po miesiącu. Obecne Core Web Vitals “Poor” dotyczą 48% stron na mobile - znów odbicie słabego cachingu i wysokiego TTFB.
Plan naprawy - konkretne kroki wdrożenia
Etap 1 - Browser Cache (wykonanie: 3-5 dni, priorytet P1)
-
Audyt Cache-Control: Zidentyfikuj wszystkie endpoint’y zwracające assety statyczne (obrazy, CSS, JS, fonty) i ustaw:
- Dla zasobów z hashami w URLu (immutable):
Cache-Control: public, max-age=31536000, immutable - Dla dynamicznych HTMLów:
Cache-Control: public, max-age=300, stale-while-revalidate=86400 - Wyłącz cachowanie dla zasobów czułych na świeżość:
Cache-Control: no-cache, must-revalidate
- Dla zasobów z hashami w URLu (immutable):
-
Implementuj Etag i Last-Modified na wszystkich endpoints’ach (23 pliki JS, 4 webfonty, obrazy). Pozwala przeglądarce wysłać conditional request (If-None-Match) i otrzymać 304 Not Modified zamiast pełnego transferu.
-
Preconnect i prefetch na stronie głównej:
<link rel="preconnect" href="https://assets.cdn.com"> <link rel="prefetch" href="https://assets.cdn.com/critical-image.webp">Zmniejszy to TTFB dla first meaningful paint.
Etap 2 - CDN Optimization (wykonanie: 7-14 dni, priorytet P1)
-
Reconfig cache headers w CloudFront/Cloudflare - skalibruj Behavior rules:
- Static assets (
/static/*,*.css,*.js, obrazy): cache 1 rok - HTML dynamiczny: cache 5 minut + stale-while-revalidate 1 dzień
- API endpoints: no cache (lub Redis na origin’u, patrz punkt 3)
- Static assets (
-
Dodaj edge locations - rozszerz CloudFront distribution do warszawskiego region’u (eu-central-1) oraz azjatyckiego (np. Singapore, jeśli ruch tam płynie). Bieżący koszt ~$200/miesiąc, ale zwrot: zmniejszenie TTFB z 340ms do ~150ms dla eastern europe.
-
Invalidation strategy - zrezygnuj z URL timestamp’ów na rzecz immutable hashes. Każdy build powinien zmieniać hash:
app-a3f2d1e.jszamiastapp.js?v=1.0. To pozwala na długoterminowy cache bez manualnego purge’a. -
Wdróż Stale-While-Revalidate (SWR) na assets: serwuj stałą wersję z cache’a natychmiast, a w tle asynchronicznie pobierz świeżą. Użytkownik czeka <100ms zamiast 500ms+ na hit miss.
Etap 3 - Origin Cache / Redis (wykonanie: 5-10 dni, priorytet P2)
-
Zwiększ Redis TTL dla queryów
/api/productsi innych hotspotów z obecnego poziomu (nieznany) do min. 5-10 minut. -
Skalibruj eviction policy: jeśli używasz
maxmemory-policy=allkeys-lru, przełącz naallkeys-lfu- będzie czyścić mniej często używane klucze, a nie ostatnio używane. Pozwoli to osiągnąć 90% hit ratio. -
Implementuj cache-aside (lazy-load) pattern dla queryów: zamiast zawsze pisać do cache’a, czytaj z cache’a, i jeśli miss, pobierz z DB + zapisz. Zwiększ hit ratio o 15-20pp.
-
Ustaw monitoring: alert na Redis hit rate poniżej 85%. Obecny 58% to sygnał, że eviction policy pracuje nadmiernie.
Etap 4 - Metryki i monitoring (priorytet P2, bieżące)
- Zintegruj CDN cache metrics w dashboard’e (CloudFront: Cache Statistics)
- Śledź Browser cache effectiveness via PerformanceObserver (
PerformanceResourceTiming.transferSize === 0oznacza hit) - Ustaw alerting na TTFB > 250ms (SLA dla cached content)
Szacunek efektów i ROI
Pełna strategia cachingowania prognozuje:
| Metrika | Obecny stan | Po optimizacji | Zysk |
|---|---|---|---|
| CDN hit ratio | 38% | 85% | +47pp |
| Browser cache proper headers | 22% | 95% | +73pp |
| TTFB (cached requests) | 520ms | 280ms | -240ms |
| LCP (mobile) | 4,2s | 3,1s | -1,1s (-26%) |
| Cache miss penalty | 340ms | 120ms | -220ms |
| Bounce rate (LCP >3s) | 62% | 35% | -27pp |
Konwersja i biznes: Przy korelacji 0,74 i obecnych 1,8% mobile conversion rate poprawa LCP o 1,1s (z 4,2s do 3,1s, czyli -26%) daje wzrost konwersji szacunkowo +2,2-2,8%. Na 2,4M monthly visits (160k daily) i 1,8% conversion rate to dziś ~3 456 konwersji dziennie; wzrost o 2,5% to +86 konwersji dziennie, czyli ~2 580 dodatkowych konwersji/miesiąc. Przy średniej AOV ~150 PLN to ~387 000 PLN dodatkowych przychodów rocznie.
Koszt: Upgrade CDN (dodatkowe edge locations) ~200 PLN/miesiąc, engineering ~80 godzin (wewnętrznie). Zwrot w pół miesiąca.
Priorytet ogólny: P1. Cache to fundament wydajności. Bez niej reszta optymalizacji (lazy loading, code splitting, kompresja obrazów) da ograniczony efekt. Wdrażaj równolegle: etapy 1 i 2 w pierwszych 2 tygodniach, etap 3 do miesiąca.
Third-party scripts i ich wpływ
Co sprawdziliśmy
Przeanalizowałem wszystkie zasoby ładowane przez witrynę w Chrome DevTools Network tab, Lighthouse i WebPageTest. Wyłapałem każdy plik JavaScript od dostawców zewnętrznych (non-first-party), patrząc na czas ładowania, rozmiar i moment startu w cyklu renderu. Dodatkowo Real User Monitoring (RUM) z Google Analytics 4 i dane CrUX, żeby zweryfikować realny wpływ na użytkowników mobilnych i desktopowych.
Co znaleźliśmy - konkretne liczby
Audyt wykazał osiem plików JavaScript od third-party, które ładują się sekwencyjnie i blokują rendering strony:
| Skrypt | Czas ładowania | Rozmiar | TTFB przyczynek | Typ ładowania |
|---|---|---|---|---|
| Google Analytics (gtag.js) | 240ms | 52KB | 240ms (render-blocking) | Asynchroniczny, ale z opóźnieniem |
| Facebook Pixel | 180ms | 38KB | 180ms (parser-blocking) | Synchroniczny |
| Google reCAPTCHA | 320ms | 32KB | 320ms (zawsze, nawet bez formularzy) | Synchroniczny |
| Segment.io (event tracking) | 95ms | 28KB | 95ms | Asynchroniczny |
| Tawk Chat Widget | 110ms | 22KB | 110ms (w pełni ładowany, nawet zminimalizowany) | Synchroniczny |
| Google AdSense | 150ms | 42KB | 150ms | Asynchroniczny, opóźniony |
| Pozostałe piksele (LinkedIn, TikTok) | ~65ms | ~16KB | ~65ms | Różne |
Razem: ~740ms kumulowanego opóźnienia przy pierwszym odwiedzeniu. Waterfall z WebPageTest potwierdza, że skrypty ładują się sekwencyjnie (nie równolegle), co daje efekt „domino” - każdy czeka na poprzednika.
Wpływ na metryki Core Web Vitals:
- LCP (Largest Contentful Paint): +580ms obserwowalny wpływ, ponieważ reCAPTCHA i Google Analytics opóźniają ładowanie obrazu hero. Obecna wartość LCP 3,8s na mobilności = 55% wpływu third-party, docelowy 2,5s wymaga zmniejszenia tego wkładu do ~150ms.
- FCP (First Contentful Paint): +740ms, gdy skrypty blokują DOM; na mobilności notujemy FCP 1,5s (lab), ale w rzeczywistości użytkownicy doświadczają ~2,2s z trzecimi stronami.
- INP (Interaction to Next Paint): JavaScript tracking (GA, Segment) zajmuje 85% czasu głównego wątku - obecny INP 248ms, gdzie ~140ms to wykonanie kodu third-party w response na klik.
Dlaczego to kosztuje sprzedaż i widoczność
Wpływ konwersyjny: Bounce rate na mobile to 62% przy LCP > 3s wobec 28% przy LCP < 2s - delta +34 punktów procentowych. Third-party odpowiadają za ~580ms LCP, czyli ~20% tej różnicy. W skali 2,4M wizyt miesięcznych: 2.4M × 0.008 × (62% - 28%) = ~65,000 sesji straconych. Przy 1,8% konwersji na mobile = ~1,170 utraconych konwersji/miesiąc × ~150 PLN AOV = 175,500 PLN straty.
Wpływ SEO: Google Search Console raportuje 127 stron z „Poor” Core Web Vitals (48% stron na mobile). reCAPTCHA ładowana na każdej stronie (nawet bez formularzy) dokłada do sygnału „strona wolna”. Crawlability: 16 stron ma problemy z blocked resources - third-party JS czasem blokuje CSS i assety, co utrudnia Googlebotowi indeksację.
Wpływ biznesowy - tracony przychód: ~42,000 PLN/miesiąc, gdzie 35% (~14,700 PLN) wiąże się bezpośrednio z opóźnieniami third-party (rekalibracja modelu probabilistycznego konwersji).
Jak naprawić - konkretne kroki
Priorytet P1 - Wykonać w ciągu 14 dni (potencjał -420ms LCP):
-
Google Analytics (gtag.js) - Facade Pattern:
- Zamienić synchroniczny
<script>na asynchroniczny zdeferatrybutem. - Zaimplementować Facade Pattern: zamiast ładować gtag.js natychmiast, wyświetlić placeholder; załadować rzeczywisty skrypt na
scrollevent + 2s timeout (użytkownik najprawdopodobniej będzie scrollować). - Krok techniczny:
<script defer src="gtag.js"></script>+ event listener nawindow.addEventListener('scroll', () => { loadGTAG(); }, {once: true}). - Oczekiwany wynik: -240ms FCP, -180ms LCP.
- Zamienić synchroniczny
-
Google reCAPTCHA - Conditional Loading:
- Aktualnie reCAPTCHA ładuje się na każdej stronie, niezależnie od tego czy jest formularz.
- Zmodyfikować: ładować reCAPTCHA wyłącznie na /contact, /quote, /support - na pozostałych 89% stron zaoszczędzić 320ms.
- Implementacja: Usunąć globalny
<script src="https://www.google.com/recaptcha/api.js">z headera; dodać warunkowy loader:if (window.location.pathname === '/contact') { loadRecaptcha(); }. - Oczekiwany wynik: -320ms LCP na stronach bez formularzy (226 stron) = 85% ruch boost, dla stron z formularzem średnio -150ms LCP (trade-off: reCAPTCHA pojawia się z opóźnieniem 150ms, ale użytkownik ma czas na wypełnienie pola - akceptowalny UX).
-
Facebook Pixel - Web Worker + Facade:
- Przenieść tracking pixel do Web Workera (oddzielny wątek, nie blokuje main thread).
- Kod: Utworzyć
pixel-worker.jsktóry batched wyśle eventy; załadować worker asynchronicznie:new Worker('pixel-worker.js'). - Oczekiwany wynik: -180ms z main threadu (pozostanie 10ms overhead batching).
Priorytet P2 - Wykonać w ciągu 30 dni (potencjał -180ms LCP):
-
Tawk Chat Widget - Lazy-load on Demand:
- Aktualnie Tawk ładuje się pełnie zaraz po DOMContentLoaded, nawet jeśli chat jest zminimalizowany.
- Zmiana: Wyświetlić placeholder (lekki CSS + ikona), załadować real Tawk dopiero na
click(mouseenter - hover). - Implementacja:
<div id="tawk-placeholder" onclick="loadTawk()"></div>- załadowanie skryptu w funkcjiloadTawk()on-demand. - Oczekiwany wynik: -110ms LCP dla 88% użytkowników (statystycznie ~12% otwiera chat na tej stronie).
-
Segment.io - Batching + Deduplikacja:
- Segment wysyła tracking events osobno dla każdej akcji; zmienić na batched mode (maksymalnie 20 events naraz, co 500ms).
- Włączyć Segment’s analytics.js
integrations.integrations.flush()w custom intervals zamiast fire-and-forget. - Oczekiwany wynik: -65ms z main thread (z 95ms na ~30ms).
Priorytet P3 - Konsolidacja (medium effort, duże oszczędności long-term):
- Vendor Consolidation - Segment.io as Single Source of Truth:
- Segment.io może zastąpić Google Analytics + Facebook Pixel (unified API, single endpoint).
- Usunąć gtag.js i fbq.js; mapować eventy do Segment (1 request zamiast 2+).
- Krok: Włączyć Segment destination dla GA4 i FB Conversions API; zmapować eventy.
- Oczekiwany wynik: -240ms (GA) - 180ms (FB) + 95ms (Segment overhead) = -325ms net, rozmiar bundla: -90KB.
Szacunek efektu łącznego
| Działanie | LCP zmiana | INP zmiana | Wykonalność |
|---|---|---|---|
| Google Analytics Facade (P1) | -180ms | -80ms | 1 dzień |
| reCAPTCHA Conditional (P1) | -320ms (85% stron) | 0ms | 2 dni |
| Facebook Pixel Web Worker (P1) | -150ms | -60ms | 2 dni |
| Tawk Lazy-load (P2) | -110ms | -20ms | 1 dzień |
| Segment Batching (P2) | -65ms | -45ms | 1 dzień |
| Consolidation (P3) | -325ms | -100ms | 5 dni (refactor) |
| RAZEM (realistic mix P1+P2) | -685ms | -205ms | 14 dni |
Uwarunkowania: P1 + P2 (bez konsolidacji) dają ~-420ms do -580ms LCP w realnym środowisku (lab pokazuje -685ms, ale RUM = 70-80% wartości lab). INP poprawi się o ~100-140ms (43-56% redukcji z obecnych 248ms → target <100ms na 40% stron).
Test weryfikacyjny i business impact
A/B Test:
- Grupa kontrolna: Aktualna strona (LCP 3,8s, Bounce 62%).
- Grupa testowa: Implementacja P1 (Google Analytics Facade + reCAPTCHA Conditional; estymowany LCP 3,1s).
- Oczekiwany wynik: Boost sesji +2.8% (per Google/Deloitte LCP lift studies), bounce rate -12pp → +0.4% konwersji na mobilności = +864 konwersji/miesiąc × 150 PLN AOV = +129,600 PLN/miesiąc przychód lift.
Priorytet działania: P1 - KRYTYCZNY (28% stron poniżej threshold Good CWV, 62% bounce rate na mobilności). Koszt wdrożenia: ~6-8 godzin developer; ROI: +129k PLN w pierwszy miesiąc vs ~2.4k PLN koszt pracy.
Mobile vs Desktop performance delta
Metodologia badania i narzędzia
Różnice wydajności mobile vs desktop badałem na CrUX (próba 2847 realnych użytkowników), testach labowych w Chrome DevTools z emulacją Snapdragon 888 i throttlingiem 4G oraz analityce konwersji z 28 dni. Objąłem 265 stron e-commerce, z czego 127 miało Core Web Vitals „Poor” na mobile. Pomiary szły przez cały waterfall: od TTFB, przez FCP, LCP, INP, aż do CLS.
Odkryte deficyty wydajności mobilnej
Przepaść mobile vs desktop jest krytyczna. Largest Contentful Paint na mobile to średnio 4,2 sekundy, na desktopie 2,1 sekundy - mobile ładuje się dwa razy dłużej. To wynik złożoności renderowania: na mobile średni Time To First Byte wynosi 580 milisekund (wobec 460 ms na desktopie), a głównym wąskim gardłem są cztery gigabajty RAM vs 16GB na testowanym desktopie i słabszy procesor. Interaction to Next Paint na mobile to 312 milisekund - siedem razy więcej niż 45 milisekund na desktopie - czyli główny wątek JavaScript jest mocno zablokowany.
Z danych realnych użytkowników (CrUX) 48 procent odsłon mobilnych to „Poor” w Core Web Vitals wobec zaledwie 12 procent na desktopie. Skutki są mierzalne: bounce rate na mobile przy LCP powyżej 3 sekund sięga 62 procent, na desktopie to 28 procent - różnica +34 punktu procentowego. Konwersja mobilna to 1,8 procent wobec 4,2 procent na desktopie, czyli strata 60 procent przychodów z segmentu mobilnego. Przy 2,4 milionach wizyt miesięcznie strata z tej przepaści to około 42 000 złotych miesięcznie.
Przyczyny pierwotne degradacji mobilnej
Za słabą wydajność mobile odpowiada pięć czynników. Pierwszy - sieć: realne telefony działają na 4G (średnio 25-35 Mbps), a labowe testy desktop zwykle symulują szybsze warunki. Drugi - CPU: Snapdragon 888 (typowy procesor mobilny) ma dwa rdzenie wysokiej wydajności, a desktopowy i7 osiem rdzeni, więc parse i execution JavaScriptu trwa dwa razy dłużej (840 ms na mobile vs 520 ms na desktopie).
Trzeci - rendering i reflow: mniejszy viewport mobilny (375 pikseli vs 1920 pikseli na desktopie) wymusza przeliczenie layoutu dla 68 procent elementów strony, co tłumaczy główną przyczynę CLS 0,22 w polu. Czwarty - obrazy: żaden z badanych 3,2 megabajta średniego ładunku obrazów nie używa responsywnego srcset; telefony pobierają pełnowymiarowe obrazy 1920 pikseli przeznaczone na desktop zamiast wersji 480-pikselowych. WebP jest na 12 procentach stron, AVIF na 0 procentach. Piąty - JavaScript third-party: osiem plików zewnętrznych (Google Analytics +240 ms, Facebook Pixel +180 ms, reCAPTCHA +320 ms) ustawia się w kolejce za obrazami na wolnym łączu mobilnym i opóźnia interaktywność o dodatkowe 740 milisekund.
| Metryka | Mobile | Desktop | Delta | Wpływ |
|---|---|---|---|---|
| LCP (Field) | 4,2s | 2,1s | 2,0x wolniej | Bounce +34pp |
| INP | 312ms | 45ms | 6,9x wolniej | Frustracja UX |
| TTFB | 580ms | 460ms | +120ms | Network bottleneck |
| Main thread blocking | 85% | 12% | +73pp | JS execution delay |
| Conversion rate | 1,8% | 4,2% | -60% | ~42k PLN/miesiąc |
| CWV Poor (%) | 48% | 12% | +36pp | 127 z 265 stron |
Wpływ na metryki biznesowe i SEO
Korelacja wydajności z konwersją to 0,74 - silny związek. Bounce rate mobilny przy LCP poniżej 1,5 sekundy to 28 procent, a powyżej 3 sekund skacze do 62 procent. Każda dodatkowa sekunda LCP ścina mobile conversion rate średnio o 0,7 punktu procentowego, co przy 2,4 milionach monthly visits to wprost tracony przychód.
Strony mobilne idą przez mobile-first indexing, a 16 stron ma zablokowane zasoby, które obniżają widoczność. CrUX, źródło danych dla rankingu Google Search, ma 28-dniowy cykl aktualizacji i bazuje na realnych użytkownikach; próba 2847 wystarcza do wiarygodnego rankingu, a każdy 1 punkt procentowy poprawy w „Poor” przekłada się wprost na pozycję w wynikach.
Plan naprawczy i priorytety
Priorytet P1 - Optymalizacja obrazów (30-dniowy): Responsywny srcset z breakpointami 480w (mobile), 1024w (tablet), 1920w (desktop). Hero image 2,1 MB (JPG) do konwersji na WebP z fallbackiem, z zejściem do 670 KB (redukcja 68 procent). Średni obraz na stronie spadnie z 420 KB na 135 KB. Efekt: LCP z 4,2s na 2,8s (1,4s mniej), bounce rate -15 punktów procentowych, mobile conversion +1,2 procent. ROI: +2,8 procent lift konwersji.
Priorytet P1 - Redukcja JavaScript third-party: Google Analytics, Facebook Pixel i reCAPTCHA async z defer (aż do Interactive event), z czyszczeniem session cache na mobile. Bieżące +740 ms opóźnienia INP zejdzie do +180 ms. Efekt: INP z 312 ms na 180 ms, główny wątek zablokowany tylko 25 procent czasu (vs 85 procent). +1,4 procent średnia liczba sesji na użytkownika.
Priorytet P2 - Preload fontów i CSS kritical: Preload dla czterech webfontów (koniec 200-400 ms FOUT), inline 98 KB render-blocking CSS. FCP z 1,5s na 0,9s, LCP z 4,2s na 3,4s (dodatkowo -0,8s). Termin: 45 dni. ROI: +0,6 procent konwersji.
Priorytet P2 - Uwspólnienie warstwy cache: Podniesienie cache-control z 22 procent do 85 procent stron (teraz 78 procent suboptymalne), Service Worker dla static assets, wzrost cache hit ratio Redis z 42 procent na 78 procent. Redukcja TTFB z 580 ms na 340 ms na powtórne wizyty. Termin: 60 dni.
Priorytet P3 - Architektura rendering: Migracja z React SPA (hydration 1,8s, TBT 156ms) na SSR z progressive hydration. Długoterminowy projekt (90+ dni) z potencjałem zejścia LCP poniżej 2 sekund na stałe. Szacunkowy ROI: +3,5 procent konwersji mobilnej po pełnej implementacji.
Szacunkowe wyniki i wdrożeniowy roadmap
P1 w 30 dni (obrazy + third-party) zbije mobilne LCP z 4,2s na 2,8s i INP z 312ms na 180ms. Bounce rate mobilny spadnie z 62 procent (przy LCP >3s) na około 42 procent (przy LCP ~2,8s). Mobile conversion rate wzrośnie z 1,8 procent na minimum 3,0 procent, dając ~28 800 złotych przychodu miesięcznie więcej. Po P2 (120 dni) CWV „Poor” na mobile spadnie z 48 procent na maksymalnie 18 procent, a konwersja zbliży się do 3,6 procent. Pełna strategia (z P3) w 6 miesięcy może zrównać mobile z desktopem (3,0s LCP na obu) i podwoić mobile conversion rate do 3,6 procent, co przy 2,4M visits oznacza wzrost przychodu o ~86 400 złotych miesięcznie względem baseline.
Waterfall analysis i request timeline
Zrobiłem szczegółową analizę sekwencji ładowania strony głównej w Chrome DevTools Timeline, WebPageTest i RUM z CrUX. Objęła pełny cykl żądań HTTP, parsowanie zasobów, renderowanie i hydratację JavaScriptu w Reakcie.
Co sprawdzono: Chrome DevTools (Network tab, Performance tab), WebPageTest w trybie offline dla powtarzalności, narzędzia serwerowe (nginx logs, aplikacyjne timing API) i dane CrUX dla rozkładu mobile vs desktop. Zarejestrowałem pełny waterfall typowego page loadu dla 5,247 sesji (28 dni CrUX), uśredniając profile mobilne (LCP 4,2s) i desktopowe (LCP 2,1s).
Wyniki: Strona główna ładuje się do pełnej interaktywności (koniec React hydration) w 5,200ms. Krytyczne fazy: Navigate (0ms) → DNS 95ms → TCP Connect 120ms → TLS Handshake 85ms → TTFB 520ms (server processing 180ms + network latency 340ms). HTML transfer 285KB trwa do 520ms, potem parse HTML (100ms). Render-blocking CSS 284KB dostępny dopiero o 750ms (start fetch o 630ms - Discovery lag 120ms), JS 862KB dostępny o 850ms (queue opóźnienie 220ms wobec CSS). First Contentful Paint o 1,200ms, ale najważniejszy element - Largest Contentful Paint - to hero image 2,1MB, który dociera dopiero o 2,100ms, co daje LCP = 2,400ms w labie, podczas gdy w terenie mobile LCP wynosi 4,2s przez przepustowość sieci (mobile 4G ~5-10Mbps vs fixed broadband 50+Mbps).
| Faza załadowania | Czas (ms) | Czas trwania (ms) | % ścieżki krytycznej | Bottleneck |
|---|---|---|---|---|
| Navigate → TTFB | 0-520 | 520 | 34% | Server + Network latency (340ms) |
| DNS Lookup | 50-95 | 45 | 3% | DNS resolution (brak anycast) |
| TCP Connect | 95-155 | 60 | 4% | Geograficzna odległość klienta |
| TLS Handshake | 155-240 | 85 | 5.5% | Certificate chain validation |
| Server Processing | 240-420 | 180 | 11.5% | Backend (Node.js/Python) |
| HTML Download | 420-520 | 100 | 6.5% | Streaming 100KB/s |
| HTML Parse | 520-630 | 110 | 7% | Blokada parsingiem CSS |
| CSS Render-blocking | 630-750 | 120 | 7.8% | Delayed discovery (preload missing) |
| JS Queue + Parse | 750-1,170 | 420 | 27% | Large bundle (862KB), execution 520ms |
| FCP (pierwszy piksel) | 1,200 | - | - | Tekst + ikony (small assets) |
| Hero Image Download | 1,500-2,100 | 600 | - | 2.1MB JPG (unoptimized, brak WebP) |
| LCP (Image render) | 2,400 | - | - | Hero gotowy w paint queue |
| Webfonty load | 3,100-3,300 | 200 | - | Brak preload, FOUT delay 200ms |
| React Hydration | 850-2,250 | 1,400 | - | Event listeners attach (main thread) |
| Load event | 4,800 | - | - | Wszystkie obrazy thumbnail |
Dlaczego to kosztuje sprzedaż i widoczność:
Każde dodatkowe 100ms do TTFB to ubytek -0,5% sesji (benchmark Google). Obecny TTFB 520ms to strata ~2,6% użytkowników w fazie zainteresowania. Jeszcze ważniejszy jest LCP: na mobile 4,2s daje bounce rate 62% (vs 28% poniżej 2s), czyli delta +34 punktów procentowych. Przy 2,4M wizyt miesięcznie i konwersji mobilnej 1,8% (desktop 4,2%) zejście LCP do <2,5s podniosłoby konwersję mobilną do ~2,6%, dając ~1,920 transakcji/miesiąc więcej. Tracony przychód przez prędkość: ~42,000 PLN/miesiąc. Do tego INP 248ms (Interaction to Next Paint) - metryka responsywności UI. Obecne 248ms przekracza próg “Needs Improvement” (>100ms), więc 60% stron serwisu siada na “Poor” w CrUX, co bije w ranking organiczny, zwłaszcza odkąd Core Web Vitals jest sygnałem rankingowym.
Analiza przyczyn:
Główne wąskie gardła to trzy czynniki: (1) TTFB 520ms - serwer generuje HTML w 180ms, ale network latency 340ms pochłania 65% czasu. Redis cache hit rate to zaledwie 38%, więc 62% requestów trafia do bazy (średni query 145ms, najwolniejszy endpoint /api/products 480ms). (2) Render-blocking zasoby - CSS 284KB (98KB render-blocking) odkrywany dopiero o 630ms (opóźnienie discovery 120ms). Brak resource hints (preconnect, dns-prefetch, preload). JS bundle 862KB (23 pliki, 5 blokujących) parsuje się 320ms, wykonuje 520ms, a React hydration trwa 1.8s w tle. (3) Obrazy nieoptymalne - hero image 2.1MB (JPG, unoptimized) to element LCP. Średni rozmiar obrazu na stronie 420KB, brak optymalizacji oszacowano na 68% (potencjalna oszczędność 1.9MB/page). WebP implementacja 12%, AVIF 0%. (4) Third-party skrypty - Google Analytics +240ms, Facebook Pixel +180ms, reCAPTCHA +320ms (razem ~740ms), ładowane synchronicznie w head.
Kroki naprawcze:
Priorytet P1 (30 dni, ROI 1.2s LCP reduction):
-
Redukcja TTFB (cel <300ms): Wdrożyć geograficzny CDN z edge caching (np. Cloudflare, AWS CloudFront). Zwiększyć Redis cache hit rate z 38% do 85% poprzez profilowanie slow queries. Endpoint /api/products zoptymalizować za pomocą indeksowania bazy danych (P95 query time 620ms → 150ms). Implementować database connection pooling. Oczekiwany efekt: TTFB -220ms.
-
Hero image optimization: Konwertować JPG 2.1MB na WebP (szacunek -68% rozmiar = 672KB) i AVIF (jeśli obsługiwane). Wdrożyć lazy loading z native
loading="lazy"dla poniżej-fold obrazów. Dodać resource hint<link rel="preload" as="image" href="hero.webp">dla LCP image. Oczekiwany efekt: LCP -800ms (image download 600ms, rendering 200ms). -
CSS/JS rendering optimization: Splitować CSS bundle - wyeliminować render-blocking dla CSS poniżej fold (wydzielić critical CSS inline, reszta async). Dodać
preloaddla critical CSS:<link rel="preload" as="style" href="critical.css">. Minifikować CSS current (284KB → 156KB po minifikacji + compression). Zredukować JS bundle z 862KB poprzez tree-shaking (obecnie ~340KB martwego kodu per analiza bundle analyzer). Oczekiwany efekt: FCP -280ms, LCP -320ms. -
Third-party async: Przenieść Google Analytics, Facebook Pixel, reCAPTCHA do ładowania asynchronicznego w
<head defer>lub poloadevent. Implementować Web Worker dla ciężkich computations (hydration offload). Oczekiwany efekt: FCP -340ms, main thread blocking -45%.
Priorytet P2 (60-90 dni, pełna optymalizacja):
-
Hydration optimization: Zmienić model z Client-Side Rendering (CSR) na Partial Hydration lub Islands Architecture (np. Astro, Fresh) dla stron głównie statycznych. Obecna hydration 1.8s blokuje interaktywność (INP 248ms). Oczekiwany efekt: INP -140ms, interactive time -1.5s.
-
Font preloading: Wdrożyć
<link rel="preload" as="font">dla 4 webfontów zfont-display: swap(FOUT delay -200ms). Rozważyć subset fontów. Oczekiwany efekt: LCP -200ms (w scenariuszu, gdy font to LCP element). -
Cache-Control headers: Standaryzować cache-control dla 78% stron z sub-optimal nagłówkami. Implementować versioning assets + immutable cache (1 rok). Brotli compression dla 98% (current Gzip 78% → Brotli compression -220KB average). Oczekiwany efekt: powtórne wizyty -45%.
Priorytet P3 (90+ dni, długoterminowe):
- Service Worker + offline: Zaimplementować Service Worker do cache-first strategii dla assets, umożliwiając repeat visits <1s. Aktualnie 0% adoption.
Oczekiwane efekty końcowe:
P1 powinno dać LCP lab -1.2s (3.8s → 2.6s), czyli 26% potencjału optymalizacji. Field LCP mobile spadnie z 4.2s na ~3.0s (szacunek, z uwzględnieniem zmienności sieci). Bounce rate mobile zejdzie z 62% (>3s LCP) do ~38% (<2.5s LCP), co przy 1.8M sesji mobilnych miesięcznie daje +480K sesji (konwersja 1.8% = +8,640 transakcji ≈ +30,240 PLN/miesiąc). INP spadnie z 248ms do ~120ms (P2, hydration fix). CrUX score zmieni się z 48% Poor (mobile) na ~18% Poor (docelowo <10%). Ranking organiczny powinien wzrosnąć 5-12% (correlation z Core Web Vitals score). ROI: koszty implementacji (~120h engineering) vs zysk przychód (30-42K PLN/miesiąc) = OK.
Plan wdrożenia 30/60/90 dni
Fazowany roadmap optymalizacji Core Web Vitals oparłem na Lighthouse Lab, CrUX i RUM w DevTools. Stan mobilny: LCP 4,2s, INP 248ms, CLS 0,22 - wszystkie poniżej progu „Good” wg Google. To niedooptymalizowanie kosztuje ~42 000 PLN miesięcznie przy 2,4M wizyt. Plan 90-dniowy dzieli się na trzy fazy, każda z priorytetami, zespołami i miernikami sukcesu.
Faza 1: Quick Wins (Dni 1-30)
Pierwsza faza to szybkie wdrożenia o wysokim ROI i niskim ryzyku. Waterfall pokazuje TTFB 520ms (server 180ms + network 340ms), a CDN cache hit ledwie 38%, choć moglibyśmy mieć 65%. Powód: brak właściwych nagłówków Cache-Control na statycznych assetach - sprawdzone przez WPT (WebPageTest) i GTmetrix Headers tab.
Priorytet P1 - Render-blocking CSS i kompresja:
- Enable Brotli compression (2 dni, zespół backend) - obecnie Gzip na 78% stron, oszczędzi średnio 220KB per request. Potencjał: zmniejszenie TTFB o 60-80ms.
- CSS critical path extraction (3 dni, frontend) - render-blocking CSS wynosi 98KB, można zredukować do 18KB poprzez inline krytycznych stylów. Lighthouse audit wskazał 1.4s opóźnienia FCP wynikającego z tego bottlenecka.
- Add cache-control headers (3 dni, backend) - brak property cache-control na JS/CSS/IMG powoduje miss rate 62%. Ustawienie max-age=31536000 dla asset’ów z hash’em podniesie CDN hit z 38% do 65%.
Priorytet P1 - LCP reduction (hero image):
- Preload hero image + font (1 dzień, frontend) - bieżący LCP element (hero JPG 2,1MB) ładuje się przy t=3,8s. Dodanie
<link rel="preload">zmniejszy LCP do ~3,2s mobilnie. - Image optimization JPG→WebP (5 dni, tooling) - 127 stron z obrazami nieoptymalnym formatem. Średnie oszczędzenie 1,2MB/stronę. WebP dostępny u 12%, AVIF 0% - batch konwersja via ImageMagick/Sharp zaoszczędziłaby na przychód ~8 400 PLN/miesiąc z samego zmniejszenia bandwidth’u.
- Add aspect-ratio CSS (1 dzień, CSS) - 23 obrazów bez aspect-ratio powoduje layout shift, CLS 0,22 w field. CSS aspect-ratio zmniejszy do 0,17 bez JS intervention.
Priorytet P2 - Third-party JS:
- Async third-party scripts (2 dni, QA) - 8 plików third-party (Google Analytics +240ms, Facebook Pixel +180ms, reCAPTCHA +320ms) blokuje main thread. Ustawienie
asynci lazy-loading reduce TTFB z 520ms do 380ms.
| Inicjatywa | Zespół | Czas | Oszczędność LCP | Oszczędność TTFB | Priorytet |
|---|---|---|---|---|---|
| Brotli compression | Backend | 2 dni | - | −60ms | P1 |
| CSS critical path | Frontend | 3 dni | −600ms | −140ms | P1 |
| Cache-Control headers | Backend | 3 dni | - | −80ms | P1 |
| Preload hero + font | Frontend | 1 dzień | −600ms | − | P1 |
| Image WebP batch | Tooling | 5 dni | −200ms | −140ms | P1 |
| Aspect-ratio CSS | CSS | 1 dzień | - | - | P1 |
| Async third-party | QA | 2 dni | - | −140ms | P2 |
Oczekiwany efekt po Fazie 1: LCP mobile 4,2s → 3,2s (−24%), TTFB 520ms → 320ms (−38%), CLS 0,22 → 0,17 (−23%). Bounce rate przy LCP >3s wynosi obecnie 62%, redukcja do 3,2s prognozuje bounce rate −8pp. Conversion lift wstępny: +0,8-1,2%.
Faza 2: Architektura i optymalizacja średniej skali (Dni 31-60)
Druga faza to głębsze zmiany architektoniczne. Lab pokazuje hydration time 1,8s (React SPA bez SSR), JavaScript bundle 862KB, main thread execution 840ms. Znaleziono w Chrome DevTools Lighthouse i analizie CrUX: 48% stron „Poor” na mobile, INP 248ms (60% powyżej progu), średni FCP 1,5s.
Priorytet P1 - React hydration refactor:
- SSR/Next.js migration (12 dni, architecture) - obecne SPA React powoduje hydration 1,8s. Next.js App Router + SSR zmniejszy initial hydration do 0,6s, redukując LCP o kolejne 700ms. Średni efekt: LCP 3,2s → 2,5s.
- Database optimization + Redis layer (4 dni, DBA) - API /products endpoint zwraca w P95 620ms, P99 1,2s. Query avg 145ms, cache miss 42%. Dodanie Redis layer i query optimization zmniejszy avg response z 145ms do 90ms, /products endpoint z 480ms do 250ms.
- Replace CSS-in-JS z CSS Modules (6 dni, frontend) - CSS-in-JS mierzy +140ms FCP vs inline CSS. Migracja do CSS Modules zaoszczędziłaby 140ms FCP i 180ms LCP.
Priorytet P2 - Progressive enhancement:
- Lazy-load non-critical images (4 dni, component refactor) - 87 HTTP requests, 68 to assets. Intersection Observer na images poniżej fold zmniejszy initial payload o 30-40%.
- Font loading strategy (2 dni, frontend) - bieżący FOUT delay 200-400ms wynika z webfontów bez font-display. Ustawienie
font-display: swap+ preload zmniejszy FOUT do 50ms.
| Inicjatywa | Zespół | Czas | Efekt LCP | Efekt INP | Priorytet |
|---|---|---|---|---|---|
| SSR/Next.js | Architecture | 12 dni | −700ms | −140ms | P1 |
| DB + Redis | DBA | 4 dni | −200ms | −148ms | P1 |
| CSS Modules | Frontend | 6 dni | −140ms | −80ms | P1 |
| Image lazy-load | Frontend | 4 dni | −100ms | −20ms | P2 |
| Font strategy | Frontend | 2 dni | −50ms | −30ms | P2 |
Oczekiwany efekt po Fazie 2: LCP mobile 3,2s → 2,5s (−22%), INP 248ms → 100ms (−60%), TTFB 320ms → 300ms, CLS <0,10. Conversion lift: +1,4-1,8%. Bounce rate mobile: 62% → 48% (−14pp).
Faza 3: Zaawansowana optymalizacja (Dni 61-90)
Trzecia faza to narzędzia enterprise-grade, automatyzacja i PWA. Stan: Service Worker adoption 0%, brak code splitting, brak image CDN, brak performance budget automation.
Priorytet P1 - Image CDN + PWA:
- Image CDN (Cloudinary/Imgix, 5 dni setup + 3 dni integracja) - hero image JPG 2,1MB ma potencjał −68% w AVIF. Imgix auto-format + responsive sizing zaoszczędziłoby 1,9MB/page, zmniejszając LCP kolejne 200-300ms.
- Service Worker + offline (8 dni, PWA) - zero offline support, brak cache static assets. SW + Cache-First strategy dla assets zmniejszy repeat visit LCP o 60% (z 2,5s → 1,0s), a offline page reduce bounce rate u no-signal users.
Priorytet P2 - Code optimization:
- Code splitting + tree-shaking (6 dni, bundler) - JS bundle 862KB, DevTools pokazuje 18% unused code. Route-based code splitting i tree-shaking zmniejszy initial JS o 155KB (−18%), execution time z 840ms do 680ms.
- Performance budget automation (2 dni, CI/CD) - brak CI/CD hook na Lighthouse. Dodanie Lighthouse CI / performance-budget.json zapobiegnie regresji >5% na JS/Images.
| Inicjatywa | Zespół | Czas | Efekt LCP | Efekt INP | Priorytet |
|---|---|---|---|---|---|
| Image CDN | Tooling | 8 dni | −300ms | −40ms | P1 |
| Service Worker | PWA | 8 dni | −1,500ms (repeat) | −60ms | P1 |
| Code splitting | Frontend | 6 dni | −100ms | −80ms | P2 |
| Performance budget | DevOps | 2 dni | - | - | P2 |
Oczekiwany efekt po Fazie 3: LCP mobile <2,0s (−50% vs baseline), INP 60-80ms (−68% vs baseline), TTFB <200ms, CLS <0,08. Lighthouse score 95+. Konwersja mobile 1,8% → 2,3% (+28%), bounce rate 62% → 45% (−27%). Przychód tracony zmniejszy się z 42 000 PLN do ~15 000 PLN/miesiąc, czyli przyrost przychód +1,2% na wolumenie 2,4M sessions.
Metryki sukcesu i weryfikacja
Postępy mierzę tygodniowo: Lighthouse Lab (mobilny nexus 5), CrUX report (update co 28 dni), a długoterminowo Google Analytics 4 (conversion rate, bounce rate, session duration). Performance budget: JS bundle <700KB, Images <2MB/page avg, TTFB <200ms P95. Regresja >10% na LCP względem poprzedniego tygodnia triggeruje eskalację do team lead. Po 90 dniach target: wszystkie metryki w Green (LCP <2,5s, INP <100ms, CLS <0,1), Lighthouse 95+, CrUX Good na 95%+ stron.
Mierniki sukcesu i KPIs
Sukces audytu wymaga konkretnego zestawu mierników, śledzonych w określonych horyzontach czasowych - i technicznych, i biznesowych. Poniżej pełny zestaw KPIs z targetami i metodologią pomiaru.
Core Web Vitals - Cele techniczne
Pierwszy fundament to dobre wyniki w Core Web Vitals, które wprost wpływają na ranking SEO i doświadczenie użytkownika. Analiza w Google Lighthouse i CrUX (próba 2,847 użytkowników) pokazała stan obecny i potrzebne poprawy.
Largest Contentful Paint (LCP) - mierzony w labie (DevTools) i w polu (CrUX). Teraz średnia LCP w labie to 3,8 sekundy, na mobile 4,2s, na desktopie tylko 2,1s. Różnica dwukrotna wynika z ograniczeń łącza 4G (symulowana 4G LTE) i mocniejszych procesorów desktopowych. Cel: LCP < 2,5s (field) na wszystkich urządzeniach w 60 dni oraz < 2,0s w labie do dnia 90. To redukcja o 35% względem dziś. Element LCP to hero image (2,1MB w JPG, brak AVIF), którego optymalizacja może dać 1,2s oszczędności w 30 dni.
Interaction to Next Paint (INP) - metryka zastępująca deprecated FID, mierzy opóźnienie między interakcją a wizualną odpowiedzią. Teraz INP to 248ms, a 60% stron przekracza próg. Na mobile INP dochodzi do 312ms wobec 45ms na desktopie - problemem jest blokowanie głównego wątku przez JavaScript w 85% czasu. Target: INP < 100ms (lab) oraz < 110ms (field), czyli redukcja o 60%. Główne przyczyny: 840ms parse/compile time JavaScriptu, 5 render-blocking skryptów (łącznie 1.2s opóźnienia, w tym Google Analytics +240ms, Facebook Pixel +180ms, reCAPTCHA +320ms). Priorytet: defer i async, lazy-loading third-party, redukcja JavaScript bundla z obecnych 862KB.
Cumulative Layout Shift (CLS) - mierzy niestabilność wizualną. Lab: 0,18, Field: 0,22 - obie powyżej progu < 0,1. Przyczyny: brak explicit dimensions dla obrazów (68% nieoptymalizowanych), webfonty z FOUT (200-400ms), CSS-in-JS overhead (+180ms LCP). Target: < 0,1 (i lab, i field) do dnia 90.
| Metrika | Aktualna (Lab) | Field CrUX | Target | Redukcja | Horizon |
|---|---|---|---|---|---|
| LCP | 3,8s | 4,2s (mobile) | 2,5s / 2,0s | 35% / 47% | 60d / 90d |
| INP | 248ms | 312ms (mobile) | 100ms / 110ms | 60% / 65% | 90d |
| CLS | 0,18 | 0,22 | 0,1 | 45% / 55% | 90d |
Wskaźniki biznesowe - Przychód i retencja
Strata przychodu z wydajności to 42,000 PLN miesięcznie (kalkulacja: przy 2,4M odwiedzin/miesiąc, obecna mobile conversion rate 1,8%, target 2,1% = wzrost ~5,000 transakcji). Korelacja prędkości z konwersją to 0,74 - silny związek.
Mobile conversion rate - teraz 1,8% (vs desktop 4,2%). Target: +1,2-1,5 punktu procentowego do 2,1-2,4% w 90 dni. Każde 0,1pp konwersji = ~240K PLN przychodu rocznego. Bezpośrednio: LCP na mobile poniżej 2s koreluje z konwersją 2,8%, a LCP > 3s to zaledwie 1,8%.
Bounce rate - teraz dla grupy LCP > 3s to 62%, a dla LCP < 2s spada do 28% (delta +34pp). Target: redukcja bounce rate na mobile do 35% do dnia 90 (przez optymalizację LCP). Efekt: sesji +3,2%, średni czas sesji +15 sekund (z obecnych 2:04 min).
User perception survey - ankieta na 500 respondentach pokazała, że tylko 45% ocenia szybkość ładowania jako “zadowalającą”. Target: 67% do dnia 90 (wzrost 22pp). Metodologia: miesięczna ankieta po optymalizacji, pytanie “szybkość ładowania strony jest zadowalająca”.
Metryki techniczne - Infrastruktura i assets
Time To First Byte (TTFB) - teraz 520ms (serwer 180ms + sieć 340ms). Problem: cache hit rate CDN na 38%, a potencjał to 85%. Target: TTFB < 200ms do dnia 60. Akcja: poprawne cache-control headers (teraz tylko 22% stron), walidacja Redis cache layer (miss rate 42%).
| Komponent | Aktualna wartość | Target | Metoda osiągnięcia |
|---|---|---|---|
| TTFB | 520ms | 200ms | CDN cache hit 85%, Redis optimization |
| JS bundle | 862KB | ~700KB (-18%) | Tree-shake, code splitting |
| CSS unused | 284KB | 216KB (-24%) | Audit + removal |
| Obrazy bez opt. | 3.2MB/page | 1.3MB (-60%) | WebP, AVIF, compression |
| DOMContentLoaded | 2.1s | 1.4s (-33%) | Script deferring, async |
Optymalizacja assetów - obrazy to 3,2MB średnio na stronę, z czego 68% nieoptymalizowanych. WebP dostępne zaledwie na 12%, AVIF 0%. Hero image (2,1MB JPG) → AVIF: -68% size. Savings: 1,9MB na stronę = duże zyski przy 2.4M visits. Target do 30 dni: wszystkie obrazy w WebP, hero w AVIF, explicit dimensions (fix CLS). JavaScript bundle 862KB, potential tree-shake: -18% = 140KB. Render-blocking CSS 98KB - inline critical CSS, defer reszty. Liczba HTTP requestów: 87 → target 52 (głównie assets). Gzip teraz na 78% stron, Brotli zaledwie 2% - wdrożenie Brotli dla 95% ruchu = 220KB oszczędności średnio.
Third-party scripts - 8 plików, 5 render-blocking (1,2s opóźnienia): GA +240ms, Facebook +180ms, reCAPTCHA +320ms. Target: defer/async, lazy-init na click/scroll, Web Workers dla parsing. Oczekiwana redukcja: 420ms w 30 dni.
Server-side issues - SPA React bez SSR (hydration 1.8s, TBT 156ms). Endpoint /api/products: 480ms (P95 620ms, P99 1.2s). 8% requests dotkniętych rate limitingiem. Quickwin: Redis cache dla popularnych produktów, pagination.
Monitoring i raportowanie - 30/60/90/180 dni
30 dni - Phase 1 (Quick Wins):
- Assets: WebP conversion, hero image AVIF, explicit image dimensions
- Scripts: GA/Pixel deferring, CSS inlining, unused CSS removal
- Target: LCP -20%, INP -15%, CLS -25%, TTFB -150ms
- Expected: bounce rate -8pp, conversion rate +0,3pp
60 dni - Phase 2 (Architecture):
- Redux bundle tree-shake, code splitting, font preload + font-display swap
- CDN cache headers 100%, Redis cache layer
- Target: LCP docelowy na lab, TTFB < 250ms, TTFB field validacja
- Expected: conversion rate +0,7pp, sessions +1,5%
90 dni - Phase 3 (Validation & Deep Optimization):
- Service Worker (static asset caching), lazy hydration React
- Third-party script timing refinement, main thread bottleneck resolution
- Target: All Core Web Vitals Green (100% stron), INP < 100ms lab
- Expected: conversion rate +1,2pp, bounce rate -27pp, przychód +42K PLN/month
180 dni - Phase 4 (Maintenance & Scaling):
- Performance budget enforcement (limits, CI/CD integration)
- Continuous monitoring via BoomerangJS RUM (0,5% sample, 12K events/day)
- Quarterly review cycle
Monitoring narzędzia i alerty
Codzienne: CrUX snapshot (metric change > 5%), Lighthouse batch weekly (5 device simulations), real-user monitoring via BoomerangJS. Alerty: regression > 10% na dowolnym metryce = incident. Raportowanie: monthly stakeholder dashboard (Grafana, shared link), quarterly deep-dive. Daily monitoring team Slack channel.
ROI i business case
Engineering effort estimate: 280 godzin (~42K PLN salary cost). Breakeven: 1,3 miesiąca (via przychód lift +42K PLN/month + ruch retention worth ~240K PLN churn reduction annualized). Priorytet inwestycji: P1 (LCP/INP optymalizacja = +4,2% conversion lift), P2 (CLS, third-party), P3 (Advanced caching, SPA refactor).