Real User Monitoring (RUM) 2026: web-vitals.js, p75 percentilek és CrUX a gyakorlatban

Hogyan állíts be saját RUM-ot web-vitals.js v5-tel: p75 percentilek, eszközosztály-szegmentáció, CrUX BigQuery lekérdezések és attribution build a valódi felhasználói teljesítmény méréséhez.

RUM 2026: web-vitals.js & CrUX útmutató

Frissítve: 2026. június 12.

A Real User Monitoring (RUM) az a folyamat, amelynek során a valódi látogatók böngészőjéből gyűjtjük a Core Web Vitals mérőszámokat (LCP, INP, CLS), majd p75 percentilis alapján elemezzük őket eszközosztály, hálózat és földrajzi régió szerint. A szintetikus tesztek (Lighthouse, WebPageTest) egyetlen, idealizált környezetben futnak, ezért nem mutatják meg, mit lát a felhasználó egy hároméves Androidon, 4G-n, akkumulátorkímélő módban. 2026-ban a Google a CrUX p75 mezei adatait használja a rangsoroláshoz, így a RUM nem opció, hanem alapkövetelmény.

  • A web-vitals.js v5 könyvtár ~2 KB gzippelt méretben szállítja az LCP, INP, CLS, TTFB, FCP méréseket, és tartalmaz egy attribution buildet a kiváltó DOM-elem azonosítására.
  • A Google a 75. percentilist (p75) használja: az oldalnak akkor jó az értékelése, ha a látogatások 75%-a a „jó" küszöb alatt van.
  • A CrUX havi adatkészlete BigQuery-ben publikus, de 100 ms (LCP), 25 ms (INP) és 0,05 (CLS) szemcsézettséggel. Finomabb felbontáshoz a CrUX API vagy saját RUM kell.
  • 2026 májusában a globális Core Web Vitals átmenőképesség 55,9% volt (CrUX, 18,4 millió origin), és asztali gépen az INP p75 jellemzően 120 ms, mobilon 248 ms körül van.
  • Az onINP({ reportAllChanges: true }) és a navigator.sendBeacon() kombináció megakadályozza az adatvesztést bfcache navigáció és tab-bezárás közben.
  • A percentilist mindig eszközosztály, csatlakozási sebesség és útvonal-sablon szerint szegmentáld. Egy aggregált p75 elrejti a valódi problémákat.

Mi az a Real User Monitoring és miért különbözik a CrUX-tól?

A Real User Monitoring lényege, hogy a saját oldalad JavaScript-kódja méri minden látogató tényleges élményét, és visszaküldi az adatokat egy elemző végpontra. A Chrome User Experience Report (CrUX) ezzel szemben a Google által, opt-in Chrome-felhasználóktól gyűjtött adat, ami 28 napos gördülő ablakban frissül, és csak Chrome-on, csak bizonyos forgalmi küszöb felett áll rendelkezésre. A különbség gyakorlati: a CrUX nem tudja megmondani, hogy a tegnap kiadott deploy elrontotta-e az LCP-t, és nem látja a Safari, Firefox vagy WebView felhasználókat.

A saját RUM-od valós időben, percenként frissül, beépíthető A/B-tesztelésbe, és tetszőleges dimenzió szerint szegmentálható: kampány, kísérlet, bejelentkezett vs. anonim, geo, eszköztípus. Tapasztalatom szerint a csapatok 90%-a addig nem talál meg INP-regressziókat, amíg saját RUM-ot nem épít, mert a CrUX p75-je túl tompa ahhoz, hogy egy 30 ms-os, csak Galaxy A-szérián jelentkező lassulást észrevegyen. (Pont egy ilyen regressziót vadásztam végig egy múltkori projektben, csak Lighthouse-szal sosem találtam volna meg.) A CrUX kiváló versenyző-benchmarkhoz, a saját RUM viszont nélkülözhetetlen a napi mérnöki munkához. A kettő nem helyettesíti egymást, hanem kiegészíti.

A szintetikus mérés (Lighthouse) ezek mellett is hasznos marad, de főleg regressziódetektálásra és diagnosztikára. A rangsorolás szempontjából a Google kizárólag a CrUX-mezőadatokra támaszkodik, így egy 100-as Lighthouse-pontszám és egy elbukott CrUX-értékelés simán együtt létezik ugyanazon az URL-en. Ha eddig csak Lighthouse-szal dolgoztál, érdemes mellé tenni egy RUM-réteget. A kettő együtt adja ki a teljes képet.

A web-vitals.js v5 beállítása lépésről lépésre

A web-vitals JavaScript könyvtár a Google Chrome csapatának hivatalos megoldása a Core Web Vitals kliens oldali mérésére. Mérete ~2 KB gzippelt, tree-shakelhető, és a PerformanceObserver API-ra épül. A telepítés egyszerű:

npm install web-vitals@5

Az alapvető használat egyetlen modulból áll, az összes metrika regisztrálva, eredmények egy sendToAnalytics() függvényen keresztül:

// rum.js — töltsd be defer attribútummal a <head> végén
import { onCLS, onINP, onLCP, onFCP, onTTFB } from 'web-vitals';

function sendToAnalytics(metric) {
  // A body egyszerre több metrikát fűzhet össze, így spóroljuk a hálózati overheadet
  const body = JSON.stringify({
    name: metric.name,
    value: metric.value,
    rating: metric.rating,           // 'good' | 'needs-improvement' | 'poor'
    delta: metric.delta,
    id: metric.id,                   // egyedi metric ID, hogy ne számoljuk duplán
    navigationType: metric.navigationType,
    url: location.pathname,
    deviceMemory: navigator.deviceMemory ?? null,
    effectiveType: navigator.connection?.effectiveType ?? null,
    ts: Date.now(),
  });

  // sendBeacon: tab-bezárás közben is megbízhatóan kiküldi az adatot
  (navigator.sendBeacon && navigator.sendBeacon('/rum', body)) ||
    fetch('/rum', { body, method: 'POST', keepalive: true });
}

onCLS(sendToAnalytics);
onINP(sendToAnalytics);
onLCP(sendToAnalytics);
onFCP(sendToAnalytics);
onTTFB(sendToAnalytics);

Két buktató, amibe sokan beleesnek. Az első: ne hívd ugyanazt az onINP() vagy onLCP() függvényt többször ugyanazon az oldalbetöltésen, mert minden hívás egy új PerformanceObserver példányt regisztrál, ami SPA-kban memóriaszivárgáshoz vezethet. A második: ha SPA-routereddel soft-navigációkat kezelsz, használd a reportAllChanges: true opciót, és figyeld a navigationType: 'soft-navigation' értéket. Részletes INP-hangoláshoz lásd az INP optimalizálás 2026 útmutatót.

Attribution build: melyik DOM-elem okozza a rossz LCP-t és INP-t?

Az alap web-vitals csomag megmondja, hogy a p75 LCP-d 4,2 másodperc, de azt nem, hogy melyik elem és melyik fázis a felelős. Az attribution build pontosan ezt adja meg. A használat ugyanaz, csak a modult cseréld le:

import { onLCP, onINP, onCLS } from 'web-vitals/attribution';

onLCP((metric) => {
  const a = metric.attribution;
  console.log({
    element: a.target,                  // a CSS selector az LCP elemre
    url: a.url,                         // az LCP forrás URL-je (kép, betűtípus stb.)
    ttfb: a.timeToFirstByte,            // szerver oldali késleltetés
    resourceLoadDelay: a.resourceLoadDelay,   // mikor kezdődött a letöltés
    resourceLoadDuration: a.resourceLoadDuration, // meddig tartott
    elementRenderDelay: a.elementRenderDelay, // főszál blokkolás render előtt
  });
});

onINP((metric) => {
  const a = metric.attribution;
  console.log({
    eventTarget: a.interactionTargetElement, // amire kattintottak
    eventType: a.interactionType,            // 'pointer' | 'keyboard'
    inputDelay: a.inputDelay,                // input → handler kezdés
    processingDuration: a.processingDuration,// handler futása
    presentationDelay: a.presentationDelay,  // commit → következő frame
    longAnimationFrameEntries: a.longAnimationFrameEntries, // LoAF entries
  });
});

Ez a felbontás megmondja, hogy az LCP-d azért 4,2 másodperc, mert a timeToFirstByte 1,8 másodperc. Vagyis nem a kép a hibás, hanem a szerver vagy a CDN. Ha viszont a resourceLoadDuration a domináns, a képformátum vagy a CDN gyorsítótár-stratégia a következő lépés. A backend késleltetés csökkentéséről részletesen írtam a TTFB optimalizálás és gyorsítótárazási stratégiák cikkben.

Az INP attribution 2026-ban kapta meg a Long Animation Frames (LoAF) integrációt, ami megmutatja, melyik script vagy stíluslap blokkolta a fő szálat egy interakció során. Ez korábban csak Chrome DevTools-ban volt látható, most már RUM-ban is. Őszintén szólva ez a legfontosabb diagnosztikai fejlesztés az elmúlt két évben.

Adatküldés sendBeacon és Visibility API segítségével

A leggyakoribb RUM-implementációs hiba, hogy a metrikákat fetch()-csel küldjük, és amikor a felhasználó bezárja a tabot, a kérés félbeszakad. Az LCP gyakran csak pagehide eseménynél véglegesedik, az INP pedig csak akkor, ha a felhasználó minden interakcióját végrehajtotta. A megoldás: navigator.sendBeacon(), ami a böngészőnek átadja a kérést, és az még tab-bezárás után is kiküldi.

// Hibás minta: fetch tab-bezáráskor elveszhet
fetch('/rum', { method: 'POST', body });

// Helyes minta: sendBeacon + visibilitychange fallback
function flush(metric) {
  const body = new Blob([JSON.stringify(metric)], { type: 'application/json' });
  if (!navigator.sendBeacon('/rum', body)) {
    // Egyes böngészők visszautasítják, ha a payload > 64 KB
    fetch('/rum', { body, method: 'POST', keepalive: true });
  }
}

// Mentsd el az utolsó értéket, és csak page-hide-kor küldd
let lastINP = null;
onINP((m) => { lastINP = m; }, { reportAllChanges: true });

addEventListener('visibilitychange', () => {
  if (document.visibilityState === 'hidden' && lastINP) {
    flush(lastINP);
    lastINP = null;
  }
});

Mit jelent a p75 és milyen percentileket érdemes követni?

A 75. percentilis (p75) azt jelenti, hogy a látogatások 75%-ánál a metrika értéke az adott küszöb alatt van. A Google hivatalosan ezt használja a Core Web Vitals értékeléséhez: az oldalnak akkor „jó" a státusza, ha p75 LCP ≤ 2,5 s, p75 INP ≤ 200 ms, p75 CLS ≤ 0,1. A p75 kompromisszum az átlag és a p95 között. Az átlagot szélsőértékek torzítják, a p95 viszont túl szigorú lenne a legrosszabb 1G-s kapcsolatokhoz.

Mérnöki munkához viszont nem elég csak a p75-öt nézni. Én a következő percentileket követem párhuzamosan: p50 (medián, a tipikus élmény), p75 (a Google rangsorolási küszöbe), p95 (a hosszú farok, ahonnan a panaszok érkeznek), és p99 (ami szinte mindig egy hiba, beragadt main thread, infinite loop, vagy egy szélsőséges eszköz). Ha a p50 és p75 közti távolság megnő, az eloszlás romlott, nem csak az átlag. Ha a p95 ugrik, de a p75 nem, akkor egy specifikus szegmens (pl. egy ország vagy egy eszköz) érintett.

MetrikaJó (p75)Fejleszthető (p75)Gyenge (p75)
LCP≤ 2,5 s2,5 – 4,0 s> 4,0 s
INP≤ 200 ms200 – 500 ms> 500 ms
CLS≤ 0,10,1 – 0,25> 0,25
FCP≤ 1,8 s1,8 – 3,0 s> 3,0 s
TTFB≤ 0,8 s0,8 – 1,8 s> 1,8 s

Eszközosztály, hálózat és útvonal szerinti szegmentáció

Egy aggregált p75 INP, amiben benne van egy iPhone 15 Pro és egy 2021-es entry-level Android, csaknem mindig hazudik. Az iPhone 15 Pro p75 INP-je tipikusan 80 ms, ugyanaz az oldalon entry-level Androidon 350 ms. Ha a forgalom 70%-a iPhone-ról jön, a p75 200 ms alatt marad. Pedig az Android felhasználók 75%-a poor INP-t lát. A megoldás: szegmentálj minden RUM lekérdezést legalább három tengely mentén:

  • Eszközosztály: használd a navigator.deviceMemory (1, 2, 4, 8 GB) és navigator.hardwareConcurrency értékeket. Nyers, de jobb, mint a User-Agent. Az „alacsony" osztály: ≤ 4 GB memória és ≤ 4 mag.
  • Hálózati osztály: navigator.connection.effectiveType ('slow-2g', '2g', '3g', '4g'). A 4G-n belüli különbség is óriási, de ennél finomabb adat csak RTT-ből és downlinkből szedhető ki.
  • Útvonal-sablon: ne pageURL szerint, hanem útvonal-sablon szerint (/termek/:id), különben minden URL külön sorba kerül, és nem aggregálható.

BigQuery- vagy ClickHouse-alapú RUM-ban az APPROX_QUANTILES(value, 100)[OFFSET(75)] adja a p75-öt szegmensenként. Egy minimális minta:

SELECT
  route_template,
  device_class,
  effective_type,
  APPROX_QUANTILES(value, 100)[OFFSET(75)] AS p75,
  APPROX_QUANTILES(value, 100)[OFFSET(95)] AS p95,
  COUNT(*) AS samples
FROM rum_events
WHERE metric = 'INP'
  AND ts >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 7 DAY)
GROUP BY route_template, device_class, effective_type
HAVING samples >= 100   -- statisztikai zaj küszöb
ORDER BY p75 DESC;

CrUX BigQuery lekérdezések valódi versenyző-összehasonlításhoz

A CrUX havi adatkészlete BigQuery-ben publikus, és ez az egyetlen mód arra, hogy lekérdezd a versenytársaid mezei teljesítményét. A táblák a chrome-ux-report projekt alatt vannak; a legfontosabb a chrome-ux-report.materialized.country_summary és az experimental.country (kísérleti metrikákhoz, pl. soft navigation). Egy egyszerű lekérdezés a saját és a versenyző origin p75 LCP-jére:

SELECT
  yyyymm,
  origin,
  device,
  p75_lcp,
  p75_inp,
  p75_cls
FROM `chrome-ux-report.materialized.device_summary`
WHERE origin IN (
  'https://sajatdomain.hu',
  'https://versenytars-egy.hu',
  'https://versenytars-ketto.hu'
)
AND yyyymm >= 202601
ORDER BY yyyymm DESC, origin, device;

2026 májusi adatok szerint globálisan az originek 55,9%-a teljesíti mind a három Core Web Vitals küszöböt; az LCP átmenőképesség 68,6%, a CLS 81,3%, az INP 86,6%. Asztali gépen az INP közel univerzálisan jó (96,8%), mobilon viszont a p75 INP médián 248 ms. Vagyis az átlagos mobiloldal a peremen táncol. Ha az oldalad mobil-domináns, a RUM-od első dolga az INP-szegmentáció legyen.

Integráció GA4, Cloudflare és Vercel Speed Insights felé

A web-vitals.js modulárisan illeszkedik bármilyen analytics rendszerhez. GA4-hez:

import { onCLS, onINP, onLCP } from 'web-vitals/attribution';

function toGA4(metric) {
  gtag('event', metric.name, {
    value: Math.round(metric.name === 'CLS' ? metric.value * 1000 : metric.value),
    metric_id: metric.id,
    metric_value: metric.value,
    metric_delta: metric.delta,
    metric_rating: metric.rating,
    debug_target: metric.attribution?.target ?? '(not set)',
  });
}

onCLS(toGA4);
onINP(toGA4);
onLCP(toGA4);

A Cloudflare Web Analytics és a Vercel Speed Insights a web-vitals.js-t építik be alapból. Ha ezek valamelyikét használod, nem kell saját végpontot is fenntartanod, de cserébe nem kapsz tetszőleges szegmentációt. Saját RUM mellett a Cloudflare/Vercel inkább redundancia és gyors A/B vizualizáció. A bfcache-helyreállítások (navigationType === 'back-forward-cache') külön kategóriát érdemelnek. Ezek látszólag villámgyorsak (LCP < 100 ms), és ha nem szűröd ki őket, hamis biztonságérzetet adnak. (Az első RUM-omnál pont ebbe szaladtam bele, hetekig azt hittem, lett valami csoda-optimalizáció.)

Gyakori RUM-implementációs hibák és elkerülésük

A leggyakoribb hibák, amiket kódellenőrzéseken újra és újra látok: (1) a script <script src="..."> blokkoló módban a <head>-ben, ami önmagában rontja az LCP-t (mindig defer vagy modul); (2) ugyanaz a metrika többször regisztrálva SPA route-változásnál, ami memóriaszivárgás; (3) hiányzó keepalive vagy sendBeacon, ami miatt a bounce-elt látogatók adata elvész; (4) elválasztatlan bfcache-helyreállítás, ami torzítja a p75-öt; (5) URL-szintű aggregáció útvonal-sablon helyett. Soft navigációknál a Speculation Rules API előtöltéssel kombinálva drámaian javítja a navigációs LCP-t, de a RUM-nak meg kell tudnia különböztetni a prerenderelt és a hagyományos navigációt. A metric.navigationType mező pontosan ezt szolgálja.

Végül egy gyakorlati tanács: a RUM-od első verziója ne akarjon tökéletes lenni. Engedj el egy MVP-t egy egyszerű végponttal és napi p75 grafikonokkal, a percentilekkel és szegmentációval később játszhatsz. Az igazi értéket az adja, hogy mostantól látod, mit lát a felhasználó, nem az, hogy szépen formázott a dashboard.

Gyakran ismételt kérdések

Mi a különbség a RUM és a szintetikus monitoring között?

A szintetikus monitoring (Lighthouse, WebPageTest) egyetlen, kontrollált környezetben futtat tesztet, ezért reprodukálható, de nem tükrözi a valós felhasználói élményt. A RUM a tényleges látogatók böngészőjéből gyűjti az adatokat, ezért lefedi az összes eszközt, hálózatot és viselkedést, viszont zajosabb és statisztikai elemzést igényel.

A web-vitals.js használata lassítja az oldalt?

Nem érdemben. A könyvtár ~2 KB gzippelt, és kizárólag passzív PerformanceObserver API-t használ. Ha defer attribútummal töltöd be, semmilyen mérhető hatása nincs az LCP-re vagy az INP-re.

Miért különbözik a saját RUM-om és a CrUX p75 értéke?

Több oka lehet. A CrUX csak Chrome-felhasználókat lát, 28 napos gördülő ablakban aggregál, és csak bizonyos forgalmi küszöb felett közöl adatot. A te RUM-od minden böngészőt mér, valós idejű, és lehet, hogy más útvonalakra szűr. Tipikusan a saját RUM volatilisabb, de időben hamarabb mutatja a regressziókat.

Hány RUM-mintára van szükség statisztikailag megbízható p75-höz?

Egy szegmensen belül legalább 100, ideálisan 1000+ minta heti szinten. 100 alatt a p75 erősen ingadozik, és napi ugrások hamis riasztásokat generálnak. Ha egy szegmensben kevesebb adatod van, vond össze egy szélesebb szegmenssel, vagy nézz heti aggregációt.

Hogyan mérjem a Core Web Vitals értéket SPA-ban soft navigációknál?

A web-vitals.js v4 óta kezeli a soft navigációkat: a navigationType: 'soft-navigation' jelzi, ha az új mérés egy kliens-oldali route-váltás után érkezik. Engedélyezd a reportAllChanges: true opciót az onINP()-nél, és a route-váltáskor bekért metrikákat külön kulcson logold, hogy ne keverd össze a hagyományos navigációkkal.

Nadia El-Sayed
A Szerzőről Nadia El-Sayed

Core Web Vitals specialist focused on real-user monitoring. Believes synthetic-only perf testing is a comforting lie.