web-vitals.js: Zbierajte Core Web Vitals z reálnych používateľov (2026)
web-vitals.js je oficiálna knižnica Chrome tímu pre RUM meranie Core Web Vitals. Sprievodca 2026 s attribution buildom, LoAF a debugom INP v produkcii.
Knižnica web-vitals.js je oficiálna JavaScriptová knižnica od Google Chrome tímu (~2 kB brotli), ktorá zbiera Core Web Vitals (LCP, INP a CLS) z reálnych používateľov v presne tej istej metodike, akú Chrome hlási do CrUX, PageSpeed Insights aj Search Console. Nasadenie trvá pár minút: nainštalujete web-vitals z npm, zavoláte onLCP, onINP, onCLS a odošlete výsledky do vášho analytického backendu cez navigator.sendBeacon. Toto je jediný spôsob, ako reálne merať INP, pretože Lighthouse ho merať nedokáže.
web-vitals v5 (2026) meria všetky tri Core Web Vitals: LCP ≤ 2,5 s, INP ≤ 200 ms, CLS ≤ 0,1 na 75. percentile reálnych relácií.
Attribution build (web-vitals/attribution) vracia konkrétny DOM element, skript alebo iframe, ktorý spôsobil zlé skóre. Bez neho v produkcii len hádate.
Od verzie 4 knižnica integruje Long Animation Frames API a pripája k INP entry pole longAnimationFrameEntries s presným rozpisom skriptov.
Dáta posielajte cez navigator.sendBeacon() v udalosti visibilitychange, nie cez fetch v unload, inak stratíte 20 až 40 % vzoriek na mobiloch.
Lighthouse a WebPageTest INP nemerajú vôbec; nahrádzajú ho TBT, ktorý s reálnym INP koreluje slabo (~0,3).
Pre SPA aplikácie zapnite reportSoftNavs: true, inak sa metriky po client-side route zmene neaktualizujú a máte klamlivo dobré čísla.
Čo je web-vitals.js a prečo je štandardom pre RUM
web-vitals.js je open-source knižnica od GoogleChrome/web-vitals, ktorá poskytuje modulárne API pre zber všetkých Core Web Vitals metrík (LCP, INP, CLS) plus doplnkových FCP a TTFB. Verzia 5, vydaná v roku 2025, priniesla plnú podporu Long Animation Frames API, presnejšie sledovanie soft navigations v SPA aplikáciách a znížila veľkosť balíka pod 2 kB (brotli).
Prečo je táto knižnica prakticky bezalternatívna? Používa presne tú istú metodiku ako Chrome pri hlásení do Chrome User Experience Report (CrUX), čo je zdrojová databáza, z ktorej Google čerpá dáta pre Page Experience ranking signál a pre PageSpeed Insights. Ak si nasadíte vlastnú implementáciu cez raw PerformanceObserver, takmer isto dostanete iné čísla. CLS napríklad používa komplexné pravidlo pre session windows, LCP musí zohľadňovať visibility state a INP vyžaduje presnú koreláciu event timestampov s frame boundaries. Toto je jeden z dôvodov, prečo z mojej praxe odporúčam nikdy neimplementovať vlastný RUM od nuly. Raz som to skúsil na jednom projekte a strávili sme dva sprinty tým, že sme sa snažili vysvetliť, prečo naše čísla nesedia s CrUX. Nesedeli, pretože sme ich zle merali.
Interne knižnica využíva PerformanceObserver s buffered: true, čo znamená, že aj keď ju načítate neskôr počas životného cyklu stránky, dostane sa k performance entries, ktoré vznikli pred jej inicializáciou. To umožňuje umiestniť volanie web-vitals nižšie v skripte bez straty presnosti.
Field data vs. lab data: prečo synthetic testy klamú
Toto je asi najdôležitejšia lekcia web performance z posledných piatich rokov: lab data (Lighthouse, WebPageTest, PageSpeed synthetic run) nie sú field data. Reprezentujú jeden test, na jednom zariadení, s jednou sieťou, v jednom momente. Reálni používatelia sa nachádzajú na desiatkach modelov telefónov, na 3G, LTE aj 5G, na CPU s obrovskou variabilitou (Snapdragon 8 Gen 3 verzus 6-ročný MediaTek). Preto Google pri hodnotení Page Experience používa výlučne field data z CrUX na 75. percentile 28-dňového kĺzavého okna.
Ešte konkrétnejšie: Lighthouse INP vôbec nemeria. Nevie kliknúť, nevie skrolovať, nevie tapnúť. Nahrádza ho metrikou Total Blocking Time (TBT), ktorá s reálnym INP koreluje slabo (Pearson ~0,3 podľa DebugBear datasetu z 2025). V mojej praxi som videl stránky s TBT pod 100 ms a p75 INP nad 500 ms. Dôvod boli synchrónne fetch handlery po kliknutí, ktoré Lighthouse jednoducho nevyprovokuje.
Pravidlo z terénu: vždy p75, vždy segmentované podľa device class. Priemery zamaskujú tail, medián zamaskuje mobilných používateľov, ktorí často tvoria 55 až 70 % traffiku. Ak si chcete zopakovať, prečo je 75. percentil zvolený a čo znamená pre organické umiestnenie, pozrite si detailne rozobratú optimalizáciu INP metriky, kde vysvetľujem prahové hodnoty aj typické zdroje regresií.
Inštalácia a základné použitie web-vitals v5
Inštalácia je štandardná npm operácia. Odporúčam pinovať verziu, pretože API sa medzi major verziami menilo (v3 → v4 sa napríklad odstránil FID handler a doplnil onINP).
npm install web-vitals@^5.0.0
# alebo cez CDN:
# <script type="module">
# import { onLCP, onINP, onCLS } from 'https://unpkg.com/web-vitals@5?module';
# </script>
Minimálny setup, ktorý zbiera všetky tri Core Web Vitals plus doplnkové FCP a TTFB, vyzerá takto:
// web-vitals-init.js
import { onLCP, onINP, onCLS, onFCP, onTTFB } from 'web-vitals';
// Jedna funkcia, ktorá odošle metriku na váš beacon endpoint.
// Používame sendBeacon, pretože prežije aj tab close/navigáciu.
function reportMetric(metric) {
const body = JSON.stringify({
name: metric.name, // 'LCP' | 'INP' | 'CLS' | 'FCP' | 'TTFB'
value: metric.value, // číselná hodnota v ms alebo unitless (CLS)
rating: metric.rating, // 'good' | 'needs-improvement' | 'poor'
delta: metric.delta, // rozdiel oproti predchádzajúcemu reportu
id: metric.id, // unikátne ID metriky per page load
navigationType: metric.navigationType,
// Kontext, ktorý si prilepíte sami:
url: location.pathname,
connection: navigator.connection?.effectiveType,
deviceMemory: navigator.deviceMemory,
});
// sendBeacon je synchrónny z pohľadu prehliadača, ale odošle sa aj počas unload.
if (navigator.sendBeacon) {
navigator.sendBeacon('/api/rum', body);
} else {
fetch('/api/rum', { body, method: 'POST', keepalive: true });
}
}
// Zaregistrujeme callbacky. Volajú sa raz počas života stránky,
// keď je metrika považovaná za finálnu (napr. LCP pri page hide).
onLCP(reportMetric);
onINP(reportMetric);
onCLS(reportMetric);
onFCP(reportMetric);
onTTFB(reportMetric);
Ako posielať Core Web Vitals do Google Analytics 4
Ak máte na stránke GA4 tag, integrácia je priamočiara. Kľúčový trik: CLS je unitless číslo (typicky 0,001 až 0,3), ktoré GA4 chytá lepšie, ak ho vynásobíte tisícom a pošlete ako integer. Ostatné metriky sú v milisekundách, tie zaokrúhľte.
import { onLCP, onINP, onCLS, onFCP, onTTFB } from 'web-vitals';
function sendToGA4({ name, delta, id, rating }) {
// gtag musí byť globálne dostupný (načítaný cez GA4 tag).
gtag('event', name, {
// GA4 očakáva integer pre value; CLS treba škálovať.
value: Math.round(name === 'CLS' ? delta * 1000 : delta),
metric_id: id, // spojí viacero reportov tej istej metriky
metric_value: delta,
metric_rating: rating, // 'good' | 'needs-improvement' | 'poor'
metric_delta: delta,
non_interaction: true, // aby to neovplyvňovalo bounce rate
});
}
onLCP(sendToGA4);
onINP(sendToGA4);
onCLS(sendToGA4);
onFCP(sendToGA4);
onTTFB(sendToGA4);
V GA4 UI potom zaregistrujte metric_rating, metric_id a metric_value ako Custom Dimensions. Ak chcete pokročilé analýzy (napríklad p75 INP podľa krajiny a device kategórie), prepojte GA4 s BigQuery. Google poskytuje oficiálny codelab pre BigQuery a web-vitals, kde je hotový SQL na výpočet percentilov.
Attribution build: debugovanie zlého skóre v produkcii
Toto je časť, ktorú väčšina tímov preskočí, a potom sa čudujú, prečo majú „zlé INP", ale nevedia povedať kde. Štandardný build web-vitals vám povie iba hodnotu. Attribution build vám povie čo ju spôsobilo. Rozdiel je zásadný. Ja som začal používať attribution build až po tom, čo som na jednom e-shope tri týždne hľadal, prečo INP skáče cez 400 ms len na katalógovej stránke. Bez atribúcie som nemal kam sa pozrieť.
// Namiesto 'web-vitals' importujte z 'web-vitals/attribution'.
import { onLCP, onINP, onCLS } from 'web-vitals/attribution';
function reportWithAttribution(metric) {
const { name, value, id, attribution } = metric;
const debug = { name, value, id };
switch (name) {
case 'LCP':
// Konkrétny DOM element, ktorý bol LCP kandidátom.
debug.lcpElement = attribution.element; // CSS selector
debug.lcpUrl = attribution.url; // URL zdroja (napr. obrázok)
debug.timeToFirstByte = attribution.timeToFirstByte;
debug.resourceLoadDelay = attribution.resourceLoadDelay;
debug.resourceLoadTime = attribution.resourceLoadTime;
debug.elementRenderDelay = attribution.elementRenderDelay;
break;
case 'INP':
// Element, na ktorý používateľ klikol/tapol.
debug.interactionTarget = attribution.interactionTarget;
debug.interactionType = attribution.interactionType; // 'pointer' | 'keyboard'
debug.inputDelay = attribution.inputDelay;
debug.processingDuration = attribution.processingDuration;
debug.presentationDelay = attribution.presentationDelay;
// Skripty, ktoré blokovali frame, pozri sekciu o LoAF nižšie.
debug.longAnimationFrameEntries = attribution.longAnimationFrameEntries;
break;
case 'CLS':
// Element, ktorý najviac posunul layout v najhoršej session window.
debug.largestShiftTarget = attribution.largestShiftTarget;
debug.largestShiftTime = attribution.largestShiftTime;
debug.largestShiftValue = attribution.largestShiftValue;
debug.loadState = attribution.loadState; // fáza page load
break;
}
navigator.sendBeacon('/api/rum-debug', JSON.stringify(debug));
}
onLCP(reportWithAttribution);
onINP(reportWithAttribution);
onCLS(reportWithAttribution);
V praxi vám to prinesie odpovede typu: „87 % zlého LCP na kategórii /produkt spôsobuje hero obrázok /images/hero-{id}.webp, ktorý má priemerný resourceLoadDelay 1200 ms". To je akčná informácia, riešenie je preload s fetchpriority="high". Bez attribution buildu by ste sedeli nad grafom čísel a hádali. Detaily o tom, ako fetchpriority zapojiť do CDN pipeline, som rozobrala v kompletnom sprievodcovi LCP optimalizáciou.
Long Animation Frames a debugovanie INP
Long Animation Frames API (LoAF, „Lo-Af") je nástupca Long Tasks API, ktorý shipol v Chrome 123 (marec 2024) a stal sa najdôležitejším nástrojom na debugovanie INP v teréne. LoAF označuje každý animation frame dlhší ako 50 ms, a na rozdiel od Long Tasks poskytuje detailný rozpis podľa fáz frame a podľa jednotlivých skriptov, ktoré ho blokovali.
Od verzie 4 web-vitals attribution build automaticky pripojí k INP metrike všetky prekrývajúce sa LoAF entries. To znamená, že pre každú pomalú interakciu viete, ktorý konkrétny skript (aj s URL, funkciou, invoker type) blokoval main thread. Príklad štruktúry, ktorú dostanete:
V tomto reálnom príklade vidíte, že tretiu stranu tracker.js pripojenú cez onclick listener trvala 310 ms, čiže 82 % procesing durácie interakcie. Riešenie je jasné: buď skript odložiť cez scheduler.yield(), alebo ho presunúť do requestIdleCallback. Toto je presne ten typ vhľadu, ktorý synthetic testy nikdy neposkytnú, pretože ich nikto nemôže „klikať" tak, ako reálny používateľ. Detaily LoAF špecifikácie sú v oficiálnej Chrome dokumentácii Long Animation Frames API.
SPA podpora a soft navigations vo verzii 5
Toto je bolesť, s ktorou zápasí každý React/Vue/Angular tím: klasický RUM meria iba prvú (hard) navigáciu. Po tom, čo používateľ klikne na router link a stránka sa vymení client-side, vaše Core Web Vitals sa už neaktualizujú, a vy máte falošne pozitívne dáta.
Od verzie 5 web-vitals podporuje soft navigations cez opt-in flag. Interne používa Soft Navigation Heuristics, štandardizovanú detekciu SPA route zmien založenú na kombinácii history.pushState, DOM mutácii a fetch aktivite. Zapnutie:
import { onLCP, onINP, onCLS } from 'web-vitals';
// Pri každej "soft" navigácii sa callback zavolá znova s novými hodnotami.
onLCP(reportMetric, { reportSoftNavs: true });
onINP(reportMetric, { reportSoftNavs: true });
onCLS(reportMetric, { reportSoftNavs: true });
V každej reportovanej metrike bude pole navigationType nastavené na 'soft-navigation'. V analytike si ich potom môžete segmentovať oddelene od hard navigácií. Často zistíte, že váš SPA má výrazne horší INP po prvej navigácii, pretože bundler načíta ďalšie chunky a zablokuje main thread. Kombinovaná stratégia kódu (bundle splitting, prefetch, streaming) je popísaná v článku o Speculation Rules API, ktorý sa dobre kombinuje s web-vitals RUM.
Časté chyby pri zbere RUM dát
Nasadenie web-vitals.js je jednoduché; správne vyhodnotiť dáta a nespáliť sa na klasických antipattern-och je ťažšie. Toto je zoznam chýb, ktoré vidím pri každom druhom audit engagemente:
1. Priemer namiesto p75
Dashboard, ktorý ukazuje average LCP = 2,1 s, je klamlivý. Vaši p75 používatelia môžu mať 4,8 s, čo je „poor" pásmo. Vždy počítajte PERCENTILE_CONT(0.75) alebo quantile(0.75), nikdy nie AVG.
2. Nesegmentované dáta
P75 globálne cez celý web zamaskuje regresie na jednotlivých šablónach. Segmentujte minimálne podľa: page template (napr. /produkt/* vs /kosik), device (mobile/desktop/tablet), connection type (navigator.connection.effectiveType), a country. Regresia jednej šablóny sa v globálnom čísle ľahko stratí.
3. Chýbajúca keepalive/sendBeacon
Ak posielate cez fetch(...) bez keepalive: true, prehliadač request zruší pri navigácii. Straty vzoriek na mobiloch bežne 20 až 40 %. Použite navigator.sendBeacon(), alebo fetch(url, { keepalive: true }).
4. Zbieranie dát iba z produkcie (nie zo staging)
Regresiu chytíte až po deployi, keď je neskoro. Zapnite web-vitals aj na staging/preview environmentoch a sledujte diff. Osobne to zapínam aj na PR preview URL, aby sme videli, či konkrétny PR nezhoršil INP.
5. Ignorovanie navigationType
Bfcache (back-forward cache) navigácie majú typicky excelentné metriky, LCP okolo 0 ms. Ak ich nefiltrujete, umelo si vylepšíte skóre. Kontrolujte metric.navigationType === 'back-forward-cache' a takéto entries buď filtrujte, alebo reportujte samostatne. Detaily o bfcache som popísala v článku o optimalizácii TTFB, kde bfcache výrazne zlepšuje aj server-side metriky.
6. Chýbajúca korelácia s biznis eventmi
„Zlé INP na page /kosik" je poznatok. „Zlé INP na page /kosik s dopadom −18 % konverzia" je akčná biznis metrika. Do beacon payloadu pridajte anonymizované biznis kontextové polia a v analytike korelujte.
Aký je rozdiel medzi field data a lab data pri Core Web Vitals?
Field data (RUM) sú namerané z reálnych relácií skutočných používateľov s ich zariadeniami a sieťami; toto sú dáta, ktoré Google používa pre Page Experience ranking. Lab data (Lighthouse, WebPageTest) sú výsledok jedného syntetického behu v kontrolovanom prostredí. Sú užitočné pre debugging, ale INP nemerajú vôbec a s reálnymi p75 hodnotami korelujú slabo.
Prečo musím posielať web-vitals dáta cez sendBeacon a nie fetch?
Metriky ako CLS a INP sa finalizujú až pri opustení stránky (visibility hidden), kedy prehliadač zvyčajne zruší nedokončené fetch requesty. navigator.sendBeacon() je dizajnovaný presne pre tento moment: request sa dostane do siete aj počas unload. Alternatívou je fetch(url, { keepalive: true }), ktorá poskytuje podobnú záruku, ale s menšou podporou v starších prehliadačoch.
Meria web-vitals.js Core Web Vitals rovnako ako Chrome UX Report (CrUX)?
Áno, knižnica používa presne tú istú metodiku ako Chrome pri hlásení do CrUX. LCP session detection, CLS session windows aj INP interaction tracking sú implementované podľa rovnakej špecifikácie. Rozdiely medzi vaším RUM a CrUX sú typicky spôsobené odlišnou populáciou (CrUX iba Chrome používatelia s opt-in), samplingom alebo tým, že váš RUM nefiltroval bots.
Ako debugovať INP v produkcii pomocou attribution buildu?
Importujte onINP z web-vitals/attribution. Pre každú interakciu s p75+ INP dostanete interactionTarget (CSS selector kliknutého elementu), rozpis na inputDelay / processingDuration / presentationDelay, a pole longAnimationFrameEntries s presným zoznamom skriptov, ktoré blokovali frame. Tieto polia agregujte v analytike a debugujte konkrétne interakcie, nie priemery.
Funguje web-vitals.js aj v single-page aplikáciách (React, Vue, SvelteKit)?
Áno, ale musíte zapnúť reportSoftNavs: true pri každom onXxx volaní. Bez tohto flagu knižnica meria iba prvú (hard) navigáciu a po client-side route zmene vám metriky zamrznú. Pri soft navigations dostane každá metrika navigationType: 'soft-navigation', čo umožňuje segmentovanú analýzu v RUM backendu.
Aká je veľkosť web-vitals knižnice a ovplyvňuje výkon stránky?
Verzia 5 má približne 2 kB (brotli komprimované) pre štandardný build; attribution build je ~3,5 kB. Interne používa PerformanceObserver s natívnymi browser API bez polling loopov, takže CPU overhead je zanedbateľný. Knižnica využíva buffered: true, takže ju môžete načítať asynchrónne bez straty presnosti.
Praktický sprievodca skrátením TTFB pod 800 ms cez edge CDN, HTTP/3, 103 Early Hints a streaming SSR. Konkrétne konfigurácie pre Cloudflare, Nginx a Next.js 15.