Real User Monitoring Core Web Vitals w 2026: web-vitals.js, CrUX API i analytics pipeline

Praktyczny przewodnik po Real User Monitoring dla Core Web Vitals: instalacja web-vitals.js v4, atrybucja INP, sendBeacon i visibilitychange, integracja z CrUX API oraz projekt analytics pipeline z segmentacją p75.

RUM Core Web Vitals 2026: web-vitals.js + CrUX

Zaktualizowano: 2 września 2026

Real User Monitoring (RUM) Core Web Vitals to praktyka zbierania metryk LCP, INP i CLS bezpośrednio z przeglądarek prawdziwych użytkowników za pomocą biblioteki web-vitals.js oraz danych z pola Chrome UX Report (CrUX), zamiast polegania wyłącznie na wynikach syntetycznych z Lighthouse. W praktyce oznacza to instrumentację frontendu skryptem ważącym poniżej 2 KB, wysyłkę beaconów do własnego backendu lub dostawcy analityki, a następnie analizę percentyli p75 i p95 rozbitych na klasy urządzeń i typy połączeń. Bo mediana kłamie, a średnia z fast 3G to fikcja.

  • Biblioteka web-vitals w wersji 4.x dostarcza pełne API atrybucji dla LCP, INP i CLS, bez pluginów i bez czekania na zewnętrzne skrypty.
  • CrUX API zwraca dane z 28-dniowego okna, zagregowane dla p75 z segmentacją mobile/desktop i formy czynnika (form factor).
  • Progi Core Web Vitals w 2026 to LCP ≤ 2,5 s, INP ≤ 200 ms, CLS ≤ 0,1, mierzone na p75 populacji.
  • Dane syntetyczne (Lighthouse, WebPageTest) pokazują potencjał, dane z pola (RUM, CrUX) pokazują rzeczywistość. Potrzebujesz obu, ale decyzje o priorytetach podejmuj na polu.
  • Beacony wysyłaj przez navigator.sendBeacon() na visibilitychange, nie na unload. Inaczej stracisz 20–40% pomiarów z mobile Safari.
  • Segmentacja p75 po typie urządzenia i ECT (Effective Connection Type) to minimum. Bez tego jedna słaba grupa użytkowników znika w agregacie.

Dlaczego RUM, a nie tylko Lighthouse?

Szczerze, w mojej pracy z Core Web Vitals raz po raz widzę ten sam wzorzec: zespół chwali się zielonym Lighthouse'em 95/100, a CrUX pokazuje, że p75 INP dla mobile wynosi 420 ms. Jak to możliwe? Lighthouse to test syntetyczny. Jedna sesja, jedno urządzenie (emulowany Moto G4 lub odpowiednik), jedno połączenie (throttled 4G lub Slow 4G w Lighthouse 12+), zero interakcji użytkownika po pierwszym zdarzeniu wejściowym. INP wymaga rzeczywistych kliknięć, tapnięć i klawiszy, czyli czegoś, czego Lighthouse w ogóle nie mierzy w standardowym audycie.

Real User Monitoring rozwiązuje ten problem, zbierając metryki od każdego użytkownika, na każdym urządzeniu, w każdej sesji. Dane są rozproszone (długi ogon percentyla p95 pokazuje najgorsze przypadki), zagregowane w czasie (widzisz regresję z dnia na dzień po deployu) i sensownie porównywalne z tym, co użytkownik faktycznie odczuwa. Field data nie jest dodatkiem do Lighthouse'a. To Lighthouse jest dodatkiem do field data. Google ocenia twoją stronę w wyszukiwarce na podstawie CrUX, nie Lighthouse'a. To wystarczający powód, aby RUM traktować jako źródło prawdy.

Warto też pamiętać, że CrUX obejmuje tylko użytkowników Chrome, którzy wyrazili zgodę na wysyłanie statystyk. Twój własny RUM zbiera dane z każdej przeglądarki, którą wspierasz (Safari, Firefox, Samsung Internet), a to ma znaczenie, jeśli 40% ruchu masz z iOS.

Instalacja i konfiguracja web-vitals.js v4

Biblioteka web-vitals od Google Chrome team w wersji 4.x (kwiecień 2024, ostatni patch 4.2.4 w połowie 2025) waży niecałe 2 KB gzip i oferuje dwa główne buildy: standardowy oraz web-vitals/attribution z rozszerzonymi danymi diagnostycznymi. Instalacja przez npm lub CDN:

npm install web-vitals@^4.2.4

Minimalna konfiguracja mierząca wszystkie trzy Core Web Vitals plus TTFB i FCP:

// src/perf/vitals.js
import { onLCP, onINP, onCLS, onTTFB, onFCP } from 'web-vitals';

function sendToAnalytics(metric) {
  const body = JSON.stringify({
    name: metric.name,
    value: metric.value,
    rating: metric.rating, // 'good' | 'needs-improvement' | 'poor'
    delta: metric.delta,
    id: metric.id,
    navigationType: metric.navigationType,
    url: location.pathname,
  });

  // sendBeacon nie blokuje unload, fetch keepalive jako fallback
  if (navigator.sendBeacon) {
    navigator.sendBeacon('/api/vitals', body);
  } else {
    fetch('/api/vitals', { body, method: 'POST', keepalive: true });
  }
}

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

Ten skrypt załaduj asynchronicznie na końcu <body>, nigdy w krytycznej ścieżce renderowania. Sama jego obecność nie może opóźnić LCP. Jeśli to zrobi, właśnie zafałszowałeś swoje pomiary.

Atrybucja: skąd wiesz, co spowodowało słaby INP?

Sama liczba w milisekundach niewiele wnosi. Jeśli p75 INP masz 320 ms, potrzebujesz wiedzieć: który element interakcji, który skrypt, ile czasu spędzone w input delay vs processing time vs presentation delay. Do tego służy build z atrybucją:

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

onINP((metric) => {
  const { attribution } = metric;
  // attribution zawiera: interactionTarget, interactionType,
  // inputDelay, processingDuration, presentationDelay,
  // loafScripts (Long Animation Frames w wersji 4+)

  sendToAnalytics({
    name: 'INP',
    value: metric.value,
    rating: metric.rating,
    target: attribution.interactionTarget, // CSS selector
    type: attribution.interactionType,     // 'pointer' | 'keyboard'
    inputDelay: attribution.inputDelay,
    processing: attribution.processingDuration,
    presentation: attribution.presentationDelay,
    // najdłuższy blokujący skrypt z LoAF
    longestScript: attribution.longestScript?.name,
    scriptDuration: attribution.longestScript?.duration,
  });
});

Właśnie to odróżnia RUM od gadania: konkretne dane, które prowadzą do konkretnej naprawy. W jednym z projektów e-commerce, który audytowałem w Q2 2026, atrybucja pokazała, że 60% złych INP było spowodowane pojedynczym skryptem A/B testingu blokującym wątek na 180+ ms podczas kliknięcia „Dodaj do koszyka". Bez atrybucji zespół szukałby winnego w Reactcie przez trzy tygodnie. Więcej o debugowaniu wątku głównego napisałem w artykule o Long Animation Frames API.

Wysyłka beaconów: sendBeacon i visibilitychange

Największy błąd, jaki widzę w wdrożeniach RUM: metryki wysyłane w window.addEventListener('unload', ...). Na mobile Safari zdarzenie unload jest niewiarygodne. Użytkownik przełącza kartę, wraca do home screen, i beacon nigdy się nie wysyła. Prawidłowy wzorzec używa visibilitychange:

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

const queue = new Set();

function addToQueue(metric) {
  queue.add(metric);
}

function flushQueue() {
  if (queue.size === 0) return;
  const body = JSON.stringify([...queue]);
  navigator.sendBeacon('/api/vitals', body);
  queue.clear();
}

// Flush kiedy strona przechodzi w hidden, działa na iOS Safari
addEventListener('visibilitychange', () => {
  if (document.visibilityState === 'hidden') flushQueue();
});

// Fallback dla pageshow (bfcache restore) i pagehide
addEventListener('pagehide', flushQueue);

onLCP(addToQueue);
onINP(addToQueue);
onCLS(addToQueue);

Kolejkowanie jest kluczowe, ponieważ CLS i INP aktualizują się przez cały czas życia strony. Bez bufora wysyłasz dziesiątki żądań; z buforem tylko jeden beacon na koniec sesji. To także oszczędza koszt po stronie backendu, co przy ruchu 10 M sesji miesięcznie robi realną różnicę.

CrUX API: dane z pola dla całej domeny

Własny RUM daje ci szczegóły per-sesja, ale CrUX API daje ci coś, czego sam nie zbierzesz: dane z 28-dniowego okna, publicznie dostępne, dla dowolnej domeny lub URL, agregowane przez Google z opt-in'owanych użytkowników Chrome. Klucz API jest darmowy z limitem 150 zapytań na minutę. Przykładowe zapytanie o dane origin:

const CRUX_KEY = process.env.CRUX_API_KEY;

async function getCruxOrigin(origin, formFactor = 'PHONE') {
  const res = await fetch(
    `https://chromeuxreport.googleapis.com/v1/records:queryRecord?key=${CRUX_KEY}`,
    {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        origin,
        formFactor, // 'PHONE' | 'DESKTOP' | 'TABLET'
        metrics: [
          'largest_contentful_paint',
          'interaction_to_next_paint',
          'cumulative_layout_shift',
          'experimental_time_to_first_byte',
        ],
      }),
    }
  );
  const data = await res.json();
  return data.record?.metrics;
}

const metrics = await getCruxOrigin('https://example.com', 'PHONE');
// metrics.largest_contentful_paint.percentiles.p75 -> ms
// metrics.interaction_to_next_paint.percentiles.p75 -> ms
// metrics.cumulative_layout_shift.percentiles.p75 -> unitless

Uruchom to jako nocny cron i loguj p75 dla mobile i desktop. Regresja tydzień do tygodnia jest twoim wczesnym systemem alarmowym; CrUX Dashboard w Looker Studio pokazuje to samo, ale API pozwala zintegrować alerty ze Slack, PagerDuty lub Grafana. Do rozbicia LCP na podelementy warto sięgnąć po techniki atrybucji LCP z Element Timing, które dają jeszcze więcej kontekstu niż same podsumowania CrUX.

Analytics pipeline: gdzie przechowywać dane RUM?

W 2026 masz trzy sensowne opcje przechowywania metryk RUM, każdą z innym profilem kosztu i wygody:

OpcjaWłasny backend (ClickHouse/BigQuery)Vendor RUM (SpeedCurve, Sentry, Datadog)Vercel/Cloudflare Speed Insights
Koszt przy 10M sesji/mies.50–150 USD (storage + compute)500–3000 USDWliczone w plan hostingu lub 20–100 USD
Custom wymiary (route, user tier)PełneOgraniczone lub tag-basedOgraniczone
Retention danych surowychDowolne30–90 dni7–30 dni
Setup time1–2 tygodnieGodzinyMinuty
Atrybucja INP/LCPZależy od twojego koduWbudowanaWbudowana (podstawowa)
Zgodność z GDPRTwoja odpowiedzialność, pełna kontrolaZależy od dostawcyZwykle OK (dane zagregowane)

Dla większości zespołów startujących z RUM zalecam podejście hybrydowe: Vercel Speed Insights lub Cloudflare Web Analytics jako darmowy baseline plus własny endpoint dla atrybucji szczegółowej, który loguje do ClickHouse. ClickHouse absurdalnie dobrze radzi sobie z zapytaniami typu „p75 INP per route per device class per week" na miliardach wierszy, czyli dokładnie z tym, czego potrzebujesz do prawdziwej analizy. Sentry Performance ma sensowną atrybucję out-of-the-box, ale przy dużym ruchu robi się drogi.

Segmentacja percentyli i klasy urządzeń

Największa lekcja z ostatnich pięciu lat pracy z field data: agregat dla całej strony to śmieć. Jeśli patrzysz na jeden globalny p75 INP, tracisz sygnał. Musisz segmentować przynajmniej po:

  • Form factor: phone vs desktop. Mobile jest zwykle 2–3× wolniejsze na INP.
  • Effective Connection Type (ECT): navigator.connection.effectiveType zwraca „4g", „3g", „2g", „slow-2g". Grupy 3G i wolniej zwykle mają dwucyfrowy multiplier na LCP.
  • Device Memory: navigator.deviceMemory (w GB). Urządzenia z 2 GB RAM mają dramatycznie gorsze INP niż 8 GB.
  • Route/template: strona główna vs koszyk vs kategoria produktu. Regresja rzadko dotyczy wszystkiego naraz.
  • Navigation type: navigate vs back_forward vs prerender (jeśli używasz Speculation Rules API).

Doklej te wymiary do każdego beacona i przy analizie zawsze pytaj o percentyl (p75, p95, p99). Mediana ukryje 25% najsłabszych sesji, a to właśnie oni odchodzą i nie wracają. Google w rankingu Core Web Vitals patrzy na p75 populacji, więc to jest twój minimalny punkt odniesienia. Ja osobiście raportuję p75 do zespołu, ale alerty stawiam na p95, bo regresja p95 zwykle poprzedza regresję p75 o kilka dni.

Najczęstsze pułapki wdrożeniowe

Ładowanie web-vitals w krytycznej ścieżce

Jeśli import web-vitals jest częścią głównego bundle'a JS, opóźnia FCP i LCP. Wydziel go do osobnego chunku ładowanego z defer lub dynamicznym importem po load. Pomiar, który sam pogarsza pomiar, jest gorszy niż brak pomiaru.

Brak deduplikacji po SPA navigation

W aplikacjach Single-Page-App onLCP i onCLS zwracają metryki tylko dla początkowego załadowania. Do measurowania nawigacji route-based musisz sam resetować liczniki lub użyć eksperymentalnego onLCP({ reportAllChanges: true }). W Next.js App Router pomaga wbudowany useReportWebVitals hook, który obsługuje nawigację client-side. Podobne wyzwania omówiłem przy okazji optymalizacji INP z scheduler.yield.

Sampling agresywny

Kuszące jest wysłać tylko 10% ruchu. Ale przy niskim ruchu na long-tail routes zabijasz statystyczną istotność. Ja stosuję sampling adaptacyjny: 100% dla nowych deployów przez 24h, potem 20% dla ruchu stabilnego. Dla klienta z 500k sesji/dobę to nadal setki tysięcy próbek dziennie.

Ignorowanie bfcache

Restory z back/forward cache mają navigationType: 'back-forward-cache'. Jeśli nie odfiltrujesz ich w raportach LCP, zaniżysz sobie średnie (bfcache jest niemal instant). Ale odfiltrować totalnie też nie chcesz, bo bfcache to prawdziwe doświadczenie użytkownika. Loguj z flagą i analizuj obie grupy osobno.

Wysyłka na HTTP, nie HTTPS

Mieszany content blokuje beacony w Chrome. Endpoint RUM zawsze na HTTPS, nawet jeśli reszta twojej infrastruktury na to nie zasługuje.

Najczęściej zadawane pytania

Czym różni się RUM od danych syntetycznych z Lighthouse?

RUM zbiera metryki z przeglądarek prawdziwych użytkowników przez cały czas trwania sesji, na wszystkich urządzeniach i połączeniach. Lighthouse to jednorazowy test syntetyczny na emulowanym urządzeniu, bez rzeczywistych interakcji. Google używa danych z pola (CrUX, czyli forma RUM) do rankingu w wyszukiwarce, a Lighthouse to narzędzie diagnostyczne, nie źródło prawdy.

Czy web-vitals.js działa w każdej przeglądarce?

Biblioteka wysyła metryki tylko dla przeglądarek wspierających odpowiednie API (PerformanceObserver, Event Timing, Layout Instability). W praktyce oznacza to Chrome, Edge, Firefox (częściowo INP od wersji 129) oraz Safari 16.4+ dla LCP i CLS. Dla przeglądarek bez wsparcia po prostu nie otrzymasz beacona i nie ma błędu.

Jak często odświeżają się dane w CrUX API?

CrUX API zwraca zagregowane dane z ostatnich 28 dni, aktualizowane codziennie. Oznacza to, że efekt deployu widzisz stopniowo, bo pełna zmiana p75 pojawi się dopiero po 28 dniach, choć trend zaobserwujesz w ciągu tygodnia. Do szybszej obserwacji zmian użyj własnego RUM z oknem 1–7 dni.

Ile kosztuje wdrożenie własnego pipeline RUM?

Dla 10 milionów sesji miesięcznie koszt storage i compute w ClickHouse Cloud lub BigQuery to zwykle 50–150 USD miesięcznie, plus koszt czasu inżynierskiego na wstępny setup (1–2 tygodnie). Komercyjni dostawcy pokroju SpeedCurve czy Datadog RUM zaczynają się od kilkuset dolarów, ale dają gotowe dashboardy i atrybucję out-of-the-box.

Czy dane RUM podlegają RODO/GDPR?

Metryki Core Web Vitals same w sobie nie są danymi osobowymi, ale IP, User-Agent i unikalny ID sesji mogą już podlegać RODO. Bezpieczne minimum to: nie loguj IP w formie surowej (haszuj lub odrzucaj), rotuj session ID po 24h i unikaj cookies dla samych metryk perf. Visitor ID w localStorage z jasnym zapisem w polityce prywatności zwykle wystarcza.

Nadia El-Sayed
O Autorze Nadia El-Sayed

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