Jak měřit výkon webu a proč samotné skóre nestačí
Zelená devadesátka z PageSpeed Insights se skvěle vyjímá v prezentaci pro klienta. O tom, jestli web někoho reálně zdržuje, ale řekne překvapivě málo. Pojďme si projít, co skóre doopravdy počítá, proč se rozchází s daty od skutečných uživatelů a jak si postavit měření, kterému se dá věřit.
Devadesát šest bodů a nespokojený klient
Situaci zná každý, kdo někdy dělal optimalizaci na zakázku. E-shop dostane nový frontend, PageSpeed Insights ukáže na mobilu 96 bodů, snímek obrazovky putuje do závěrečné zprávy. Za tři týdny dorazí od klienta rozladěný e-mail: Search Console hlásí u LCP „Vyžaduje zlepšení“ a část adres svítí červeně. Mezitím se přitom nic nenasazovalo.
Nejde o chybu měření. Skóre a terénní data prostě odpovídají na dvě různé otázky. Skóre říká, jak rychle se jedna konkrétní stránka načetla na simulovaném telefonu někde v datovém centru. Chrome User Experience Report (CrUX) říká, jak dlouho čekali skuteční lidé, kteří na web přišli za posledních osmadvacet dní. Že se obě čísla občas potkají, je spíš shoda okolností než důkaz, že měříte správně.
Co Lighthouse ve skutečnosti počítá
Skóre výkonu není čas ani průměr časů. Je to vážený průměr pěti laboratorních metrik, z nichž se každá nejprve zvlášť převede na stupnici od nuly do stovky:
| Metrika | Váha | Co sleduje |
|---|---|---|
| Total Blocking Time (TBT) | 30 % | jak dlouho bylo hlavní vlákno zablokované |
| Largest Contentful Paint (LCP) | 25 % | kdy se vykreslil největší prvek ve výřezu |
| Cumulative Layout Shift (CLS) | 25 % | jak moc obsah pod rukama poskakoval |
| First Contentful Paint (FCP) | 10 % | kdy se objevil první obsah |
| Speed Index (SI) | 10 % | jak rychle se plnil viditelný výřez |
Důležitější než samotné váhy je způsob převodu. Naměřená hodnota se na body nepřepočítává lineárně, ale přes log-normální křivku kalibrovanou na data z HTTP Archive. Skóre tedy netvrdí „načetl ses za 2,4 sekundy“, nýbrž „jsi na tom lépe než zhruba tolik a tolik procent webů“. Z toho plynou dvě nepříjemné vlastnosti.
Zaprvé, cesta z 55 na 70 bodů bývá otázka dvou odpolední, kdežto posledních pár bodů pod stovkou umí zabrat týdny a stojí nesmyslně mnoho práce. Zadruhé, rozdíl mezi 88 a 93 body je u většiny webů čistý šum. Rozptyl pět až deset bodů mezi jednotlivými běhy je běžný; stačí, aby se požadavek trefil do jiného uzlu CDN, aby reklamní skript vrátil jinou variantu nebo aby se právě spustil garbage collector.
Stojí za to připomenout, v jakých podmínkách test probíhá. Mobilní profil znamená zpomalený procesor, simulovanou pomalou 4G, prázdnou cache, žádná rozšíření prohlížeče, žádné cookies a jedno jediné načtení. Návštěvník se nikam neproklikne, nic nevyfiltruje, nic nepřidá do košíku. Je to legitimní testovací scénář, jenže jeden z tisíců. A shodou okolností zrovna ten, který sedí na návštěvníka, jenž na web přišel poprvé v životě z Googlu na levném Androidu.
A ještě jedna věc, na kterou se běžně zapomíná: INP se v laboratoři neměří vůbec. Nemá kdo klikat. Lighthouse proto používá jako zástupný ukazatel Total Blocking Time, což je také důvod jeho třicetiprocentní váhy. Jako aproximace vytíženosti hlavního vlákna během načítání to funguje slušně. O tom, jak web zareaguje na kliknutí ve filtru katalogu po deseti sekundách používání, neřekne nic.
Mimochodem, Lighthouse mezitím dorostl do třinácté verze a zbavil se kategorie PWA. Pokud v CI porovnáváte skóre napříč lety, může být na vině prostě jiná verze nástroje.
Terén: 75. percentil, 28 dní a pouze Chrome
Terénní data (RUM, real user monitoring) měří to, co se skutečně stalo v prohlížečích návštěvníků. Google si je sbírá sám do CrUX a hodnotí podle nich tři metriky, které se od března 2024 nemění:
| Metrika | Dobré | Vyžaduje zlepšení | Špatné |
|---|---|---|---|
| LCP | ≤ 2,5 s | 2,5 až 4 s | > 4 s |
| INP | ≤ 200 ms | 200 až 500 ms | > 500 ms |
| CLS | ≤ 0,1 | 0,1 až 0,25 | > 0,25 |
Hodnotí se na 75. percentilu. To není detail, to je celá filozofie: prahem neprojdete jen proto, že je spokojený medián, ale až tehdy, když jsou v pořádku tři ze čtyř zobrazení stránky. Průměr by ocas rozdělení rozmělnil, medián by schoval polovinu problémů. Percentil naopak nutí dívat se i na tu čtvrtinu, které se web nenačetl hezky.
CrUX má ale několik vlastností, které je potřeba znát, jinak z něj budete vyvozovat nesmysly:
- Klouzavé okno 28 dní. Oprava nasazená v úterý se v číslech začne projevovat postupně a naplno až za měsíc. Přesně tady vzniká většina paniky typu „nasadili jsme to a nic se nestalo“.
- Data jen z Chromu. Desktop i Android, ale žádné Safari a žádný iOS. U českých e-shopů s dražším zbožím tak z hodnocení vypadává segment, který často dělá největší část obratu.
- Vzorkování a minimální počet měření. Málo navštěvované adresy vlastní data nemají a dostanete čísla za celý původ (origin), tedy homepage, blog i pokladnu naráz. Takový průměr se pak nedá interpretovat téměř nijak.
- Rozpad LCP na části. Od února 2025 CrUX publikuje, kolik z LCP zabralo TTFB, jak dlouho trvalo, než prohlížeč obrázek vůbec objevil, jak dlouho se stahoval a kolik času uplynulo do vykreslení. Je to nejlepší bezplatná diagnostika, jaká je k mání, protože během chvíle rozhodne, jestli problém sedí v backendu, nebo v tom, že hero obrázek dosazuje JavaScript až po hydrataci. V PageSpeed Insights to nenajdete, podívejte se do CrUX Vis nebo si data vytáhněte přes CrUX API.
Ostatní prohlížeče metriky konečně dohánějí: Firefox umí INP od verze 144, Safari je postupně přidává v rámci Interopu. Do CrUX se to ale nepromítne. CrUX je Chrome, a pokud chcete znát čísla za všechny své návštěvníky, musíte si je změřit sami.
Sedm způsobů, jak dobré skóre lže
Měříte jinou stránku, než která vydělává. Skóre se testuje na homepage, protože to je nejsnazší. Vydělává se ale v detailu produktu, ve výpisu kategorie s dvaceti filtry a v pokladně, tedy na šablonách, které mají nejvíc dat, nejvíc skriptů a nejmíň cache.
Měříte první návštěvu s prázdnou cache. Reálný provoz je směs prvních a opakovaných návštěv a v každé z nich se web chová jinak. Optimalizace, která pomůže jedné skupině, může druhé uškodit; agresivní předběžné načítání je klasický příklad.
Měříte nepřihlášeného návštěvníka. Přihlášená část webu mívá jiné dotazy do databáze, jiný poměr zásahů do cache a často výrazně víc JavaScriptu. Bývá to nejpomalejší část aplikace a v žádném veřejném měření se neobjeví.
Nevíte nic o tom, co se děje po načtení. Načtení stránky zabírá u dnešních aplikací menšinu času, který v nich lidé stráví. Rozbalení filtru, které na starším telefonu trvá 700 milisekund, do skóre nepromluví ani písmenkem.
Nevidíte rozptyl. Průměrné TTFB 180 ms vypadá výborně, dokud nezjistíte, že p95 je 2,8 sekundy, protože jednou za čas požadavek mine cache a spustí dotaz, který nikdo neindexoval. Průměry v monitoringu výkonu škodí; pracujte s percentily.
Skóre se dá vyrobit. Odložení skriptů až na první scroll, načtení analytiky po dvou sekundách, obsluha odlišné varianty stránky podle user agenta testovacího prohlížeče. Nejhezčí je „optimalizace“, kdy někdo označí hero obrázek jako loading="lazy": skóre se pohne nahoru, LCP v terénu spadne dolů. Auditem to projde. Uživateli to nepomůže.
Skóre neváží, co uživatele opravdu bolí. Sto milisekund navíc při načtení článku na blogu a sto milisekund navíc v našeptávači vyhledávání jsou dvě naprosto odlišné částky. Ve skóre skončí obojí ve stejném pytli, pokud se tam vůbec dostane.
Ke cti Googlu je třeba dodat, že tohle všechno ví. Proto ranking staví na terénních datech a proto Search Console ukazuje CrUX, a ne skóre. Zmatek vzniká hlavně u nás, protože skóre je jedno pěkně barevné číslo, které se dobře posílá e-mailem.
Vlastní RUM se dá postavit za odpoledne
Knihovna web-vitals je dnes v páté verzi a v takzvaném attribution buildu přidává i informaci o tom, který prvek za naměřenou hodnotu může. Kostra sběru vypadá zhruba takto:
import { onLCP, onINP, onCLS, onTTFB } from 'web-vitals/attribution';
const endpoint = '/api/rum';
function report(metric) {
const body = JSON.stringify({
name: metric.name,
value: Math.round(metric.value),
rating: metric.rating, // good | needs-improvement | poor
navigationType: metric.navigationType,
template: window.__TEMPLATE__, // 'product-detail', ne '/boty/nike-air-42'
build: window.__BUILD_ID__,
loggedIn: document.documentElement.dataset.auth === '1',
target:
metric.attribution?.interactionTarget ?? // INP
metric.attribution?.largestShiftTarget ?? // CLS
metric.attribution?.target, // LCP
});
if (!navigator.sendBeacon?.(endpoint, body)) {
fetch(endpoint, { body, method: 'POST', keepalive: true });
}
}
onLCP(report);
onINP(report);
onCLS(report);
onTTFB(report);
Code language: JavaScript (javascript)
Několik věcí na tom stojí za komentář.
Metriky se hlásí až ve chvíli, kdy je jejich hodnota konečná, tedy typicky při skrytí stránky. sendBeacon je proto nutnost, běžný fetch bez keepalive vám prohlížeč při odchodu klidně zruší.
Do payloadu patří šablona, ne URL. Když budete agregovat podle adres, získáte deset tisíc řádků s jedním měřením a nula použitelných percentilů. Jakmile agregujete podle šablon, uvidíte, že product-detail má p75 LCP 3,1 sekundy a article 1,4 sekundy, a hned víte, kam jít.
Identifikátor buildu je maličkost, která se vyplatí. Bez něj poznáte, že se výkon zhoršil, ale ne po kterém nasazení, a hledání viníka se protáhne na dny.
navigationType odliší běžnou navigaci od návratu tlačítkem zpět a od předvykreslené stránky. Míchat je dohromady nemá smysl. Návrat z bfcache je z principu skoro okamžitý, takže vám bude čísla vylepšovat, aniž byste pro to cokoli udělali.
Vzorkovat provoz je v pořádku, u velkých webů dokonce nutnost. Vzorkujte ale na úrovni relace, ne jednotlivých metrik, jinak dostanete uživatele, u kterého znáte LCP, ale ne INP, a nikdy nespojíte příčinu s následkem.
Data potřebují úložiště, které umí percentily nad velkým objemem řádků. ClickHouse, BigQuery nebo cokoli podobného. Klasické metrické systémy typu Prometheus se hodí hůř, protože kvantily z histogramů jsou při nízkých počtech vzorků nepřesné právě v oblasti, která vás zajímá. Pokud nechcete nic provozovat, hotových služeb je dnes celá řada a stojí zlomek toho, co vás bude stát vlastní údržba.
Server-Timing aneb most mezi backendem a prohlížečem
Nejlevnější trik, jak zjistit, kde se ztrácí čas před prvním bajtem, je hlavička Server-Timing:
Server-Timing: app;dur=88, db;dur=42, cache;desc="MISS"
Code language: JavaScript (javascript)
Prohlížeč ji zpřístupní přímo v JavaScriptu:
const nav = performance.getEntriesByType('navigation')[0];
console.table(nav.serverTiming);
Code language: JavaScript (javascript)
Díky tomu můžete do stejného záznamu, ve kterém máte LCP, přibalit i to, jestli šlo o zásah do cache a kolik času sežraly dotazy do databáze. Když vám pak p75 LCP roste, během minuty rozhodnete, jestli má práci frontendista, nebo někdo od databáze. U měření napříč doménami nezapomeňte na hlavičku Timing-Allow-Origin, jinak vám prohlížeč většinu časů zamlčí a uvidíte samé nuly.
Metriky, které vám nikdo nedodá
Standardní metriky popisují načtení. Vaše aplikace ale má vlastní okamžiky, na kterých záleží, a ty si musíte pojmenovat sami. Čas do zobrazení ceny. Čas od kliknutí na „Do košíku“ po potvrzení. Čas do prvního výsledku našeptávače. Doba překreslení kalendáře po změně měsíce.
Pro jednoduché případy stačí performance.mark() a performance.measure(), u prvků v DOM se hodí atribut elementtiming, po kterém prvek dostane vlastní záznam v PerformanceObserver. A když hledáte, co konkrétně blokuje odezvu, sáhněte po Long Animation Frames:
new PerformanceObserver((list) => {
for (const frame of list.getEntries()) {
if (frame.duration < 100) continue;
console.log(frame.duration, frame.scripts.map((s) => s.sourceURL));
}
}).observe({ type: 'long-animation-frame', buffered: true });
Code language: JavaScript (javascript)
Oproti staršímu Long Tasks API vidíte i to, který skript dlouhý snímek způsobil a kolik času padlo na styly a layout. Pro ladění INP v produkci je to zdaleka nejužitečnější věc, která v posledních letech přibyla.
Zvláštní kapitolou jsou jednostránkové aplikace. Přechod mezi „stránkami“ v SPA není z pohledu prohlížeče navigace, takže se pro něj LCP nepočítá a v CrUX po něm nenajdete stopu. Chrome na to má experiment se soft navigacemi, který od verze 147 běží v dalším origin trialu a dá se zapnout i přes chrome://flags/#soft-navigation-heuristics. Než se to usadí, měřte si přechody vlastními značkami. Jinak vám z metrik vyjde, že máte bleskový web, na kterém uživatelé čekají tři sekundy na každý druhý klik.
Poskládat to dohromady: terén, laboratoř, CI
Tyhle tři vrstvy se nevylučují, jen má každá jinou roli. Problémy začínají ve chvíli, kdy se použijí obráceně.
Terénní data hledají problém. Jenom ona vědí, na kterých šablonách, zařízeních a v jakých zemích to bolí. Začínejte tady, ať neoptimalizujete něco, co nikomu nevadí.
Laboratoř problém reprodukuje a ladí. Lighthouse, WebPageTest a panel Performance v DevTools jsou deterministické, a to je jejich obrovská výhoda. Změníte jednu věc a hned vidíte rozdíl, aniž byste čekali čtyři týdny na CrUX.
CI hlídá regrese. Tady jediná rada: netestujte skóre, testujte metriky. Skóre má takový rozptyl, že vám bude build padat náhodně a tým si do týdne zvykne prahy zvyšovat. Rozumnější je nastavit tvrdé stropy na konkrétní hodnoty a na velikost balíků:
{
"ci": {
"collect": { "numberOfRuns": 5 },
"assert": {
"assertions": {
"largest-contentful-paint": ["error", { "maxNumericValue": 2500 }],
"total-blocking-time": ["error", { "maxNumericValue": 300 }],
"cumulative-layout-shift": ["error", { "maxNumericValue": 0.1 }],
"resource-summary:script:size": ["error", { "maxNumericValue": 300000 }]
}
}
}
}
Code language: JSON / JSON with Comments (json)
Opakované běhy a medián z nich nejsou luxus, ale nutné minimum. A protože největší škody napáchá kód, který na web dostane někdo jiný než vývojáři, patří do rozpočtu i limit na skripty třetích stran. Marketingový tag manager umí LCP zhoršit spolehlivěji než jakýkoli framework.
Zbývá ověřit, že práce k něčemu byla. Případové studie o tom, že zrychlení o desetiny sekundy zvedlo konverze o jednotky procent, existují a bývají působivé, jenže je publikují výhradně ti, kterým to vyšlo. Ve vlastním prostředí to buď změřte A/B testem, nebo alespoň porovnávejte srovnatelná období a počítejte se sezónností. Bez toho zůstane vazba na byznys jen hypotézou, byť pravděpodobnou.
Kdy skóre naopak úplně stačí
Buďme féroví, Lighthouse není zbytečný. Před spuštěním nového webu je laboratorní měření jediné, které máte k dispozici, protože terénní data zkrátka nemáte odkud vzít. Na rychlou kontrolu konkurence je skóre ideální, na cizí weby se jinak nedostanete. Skvěle poslouží i jako smoke test před nasazením a jako seznam konkrétních doporučení, protože diagnostika v Lighthouse je opravdu dobrá a spousta nálezů je platná bez ohledu na to, kolik bodů se vypočítá.
Problém nastává jediný, zato spolehlivě: když se skóre stane cílem. Ve chvíli, kdy se stovka v PageSpeed Insights dostane do smlouvy nebo do osobních cílů, přestane měřit rychlost a začne měřit, jak dobře tým umí obelstít Lighthouse.
Co reportovat místo jednoho čísla
Pokud si z celého textu máte odnést jednu praktickou věc, tak tuhle: přestaňte posílat jedno skóre za celý web a začněte posílat p75 pro LCP a INP na třech nejdůležitějších šablonách, zvlášť pro mobil a desktop, spolu s podílem zobrazení v pásmu „dobré“. Do stejného grafu si vyneste značky nasazení. Pět minut práce navíc a najednou vidíte, že za zhoršením z minulého týdne stojí konkrétní release, a ne mystická vůle Googlu.
Ten graf bude nudný, málokdy se na něm něco dramatického stane a v prezentaci vypadá hůř než zelené kolečko se stovkou. Zato ale odpovídá na otázku, kterou jste chtěli položit od začátku: čekají naši uživatelé, nebo ne?
… liked this!
… reposted this!