RUM με web-vitals.js: Πλήρης Οδηγός για Real User Monitoring (2026)

Οδηγός για Real User Monitoring με τη βιβλιοθήκη web-vitals.js v4: εγκατάσταση, Beacon API, p75 ανάλυση, attribution για INP και πραγματικά παραδείγματα από production sites.

RUM με web-vitals.js: Οδηγός 2026

Ενημερώθηκε: 21 Ιουνίου 2026

Το Real User Monitoring (RUM) με τη βιβλιοθήκη web-vitals.js είναι ο μόνος αξιόπιστος τρόπος να γνωρίζεις πώς αποδίδει ο ιστότοπός σου για πραγματικούς επισκέπτες. Συλλέγει CLS, INP, LCP, FCP και TTFB απευθείας από τον browser του χρήστη και τα στέλνει στο back-end σου μέσω της Beacon API, για ανάλυση στο 75ο εκατοστημόριο. Στον παρακάτω οδηγό δείχνω πώς να εγκαταστήσεις τη βιβλιοθήκη v4 του 2026, ποια metrics συλλέγουμε, πώς αποφεύγουμε χαμένα beacons και πώς διαβάζουμε σωστά τα δεδομένα ανά κλάση συσκευής.

  • Η βιβλιοθήκη web-vitals v4 (Μάρτιος 2024+) μετρά CLS, INP, LCP, FCP και TTFB με τις callbacks onCLS, onINP, onLCP, onFCP, onTTFB.
  • Το INP αντικατέστησε το FID ως επίσημο Core Web Vital τον Μάρτιο του 2024. Αν ακόμα μετράς FID, τα δεδομένα σου είναι παρωχημένα.
  • Στείλε τα beacons με navigator.sendBeacon() κατά το visibilitychange, ώστε να μη χάνεις δεδομένα στο unload.
  • Ανάλυσε πάντα στο p75 ανά κλάση συσκευής (mobile/desktop). Οι μέσοι όροι κρύβουν τα προβλήματα της ουράς.
  • Το πεδίο navigationType διακρίνει cold loads από bfcache restores. Οι τιμές INP/LCP διαφέρουν ριζικά.
  • Το CrUX δίνει μηνιαία πανοραμική εικόνα, αλλά μόνο το δικό σου RUM σου δίνει per-route αναλυτικά για debugging.

Τι είναι το Real User Monitoring;

Real User Monitoring σημαίνει συλλογή δεδομένων απόδοσης από τους πραγματικούς browsers των επισκεπτών σου, την ώρα που χρησιμοποιούν τον ιστότοπο. Σε αντίθεση με τα synthetic εργαλεία (Lighthouse, WebPageTest) που τρέχουν ένα τεστ από έναν προκαθορισμένο server με προκαθορισμένο δίκτυο, το RUM σου δείχνει τι βιώνει ένας χρήστης στη Θεσσαλονίκη με 4G και Android Go, όπως και ένας στο Λονδίνο με fiber και M2 MacBook.

Η αλήθεια είναι ότι, στη δουλειά μου με δεκάδες production sites, σχεδόν κάθε φορά που το Lighthouse έδειχνε «πράσινα» CWV, το RUM αποκάλυπτε σοβαρά προβλήματα INP σε χαμηλής ισχύος συσκευές, ειδικά μετά την προσθήκη third-party scripts. Γι' αυτό προτείνω πάντα πρώτα RUM, μετά synthetic. Το επίσημο guide του web.dev για Core Web Vitals εξηγεί τη διάκριση field vs lab με μεγαλύτερη λεπτομέρεια.

Η βιβλιοθήκη web-vitals της Google είναι το de facto standard για RUM CWV. Είναι μικρή (περίπου 2KB gzipped), τρέχει σε όλους τους μοντέρνους browsers, και χρησιμοποιεί τις ίδιες APIs που χρησιμοποιεί και το Chrome για να γεμίσει το CrUX dataset: PerformanceObserver, Layout Instability API, Event Timing API.

RUM έναντι synthetic testing και CrUX

Πριν μπούμε σε κώδικα, αξίζει να ξεκαθαρίσουμε από πού έρχονται τα νούμερα που βλέπεις σε διαφορετικά dashboards. Οι τρεις πηγές δεδομένων CWV το 2026 είναι:

Χαρακτηριστικό Synthetic (Lighthouse) CrUX RUM (web-vitals.js)
ΠηγήΈνα emulated lab runChrome users που έχουν opt-inΌλοι οι επισκέπτες σου
ΣυχνότηταOn-demand28ήμερο rolling, μηνιαία ανανέωσηΣε πραγματικό χρόνο
GranularityPer-URL, μία τιμήOrigin-level / URL-level όπου υπάρχει trafficPer-route, per-device, per-session
AttributionFull traceΚαμίαΠλήρης με attribution build
ΚόστοςΔωρεάνΔωρεάνSelf-hosted ή SaaS
ΧρήσηPre-merge regression checksΕπίσημη βαθμολογία SearchDiagnose + alert

Το CrUX είναι αυτό που χρησιμοποιεί η Google για το ranking signal, οπότε αν εκεί έχεις «πρόβλημα», το έχεις και στην πραγματικότητα. Όμως το CrUX έρχεται με 28 ημερών καθυστέρηση και χωρίς attribution. Για να καταλάβεις γιατί έγινε bad ένα INP, χρειάζεσαι RUM. Είναι το ίδιο μοτίβο που περιγράφω και στον οδηγό βελτιστοποίησης INP: το CrUX σου λέει ότι υπάρχει πρόβλημα, το RUM σου λέει σε ποιο button.

Εγκατάσταση web-vitals.js v4

Η v4 της βιβλιοθήκης κυκλοφόρησε μαζί με την αλλαγή του INP σε Core Web Vital. Αν τρέχεις ακόμα v2 ή v3, αναβάθμισε άμεσα, καθώς οι παλιότερες εκδόσεις δεν εκθέτουν την onINP. Μπορείς να δεις τις πρόσφατες αλλαγές στο repository της Google στο GitHub.

1. Εγκατάσταση μέσω npm

npm install web-vitals@^4

2. Βασική αρχικοποίηση

Δημιούργησε ένα αρχείο rum.js και φόρτωσέ το όσο πιο νωρίς γίνεται στο <head>, με defer ώστε να μη μπλοκάρει το parsing:

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

const ENDPOINT = '/api/rum';

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,                // μοναδικό id ανά μέτρηση
    navigationType: metric.navigationType,
    url: location.pathname,
    deviceMemory: navigator.deviceMemory || null,
    connection: navigator.connection?.effectiveType || null,
    ts: Date.now(),
  });

  // Beacon API: παραδίδει αξιόπιστα ακόμη και σε page unload
  if (navigator.sendBeacon) {
    navigator.sendBeacon(ENDPOINT, body);
  } else {
    fetch(ENDPOINT, { body, method: 'POST', keepalive: true });
  }
}

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

Αποστολή με Beacon API χωρίς χαμένα δεδομένα

Το μεγαλύτερο λάθος που βλέπω σε custom RUM implementations είναι η χρήση plain fetch() χωρίς keepalive: true. Όταν ο χρήστης κάνει navigate away, ο browser μπορεί να κάνει abort το request, και χάνεις ακριβώς τα beacons του INP και CLS που οριστικοποιούνται στο τέλος του session. Honestly, μου έχει συμβεί δύο φορές σε διαφορετικά projects να ψάχνω «που χάθηκε το 30% των INP samples» και η απάντηση ήταν πάντα η ίδια: missing keepalive.

Η σωστή λύση είναι η Beacon API της MDN, η οποία σχεδιάστηκε ακριβώς για αυτή την περίπτωση. Στέλνει small POST request που εγγυάται ο browser ότι θα φύγει ακόμη και αν η σελίδα κλείνει. Από τη v3 και μετά, η web-vitals.js στέλνει τις τιμές της κατά το visibilitychange event, ώστε να ταιριάζει με τη συμπεριφορά του CrUX.

Συνδυασμός με page visibility

Αν θες να κάνεις και batching πολλαπλών metrics μαζί, χρησιμοποίησε ένα queue που flush-άρει στο pagehide/visibilitychange:

const queue = new Set();

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

function enqueue(metric) {
  queue.add({
    name: metric.name,
    value: metric.value,
    rating: metric.rating,
    id: metric.id,
    url: location.pathname,
  });
}

// flush όταν κρύβεται η σελίδα
addEventListener('visibilitychange', () => {
  if (document.visibilityState === 'hidden') flush();
});

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

Σχήμα back-end και αποθήκευση

Το endpoint /api/rum πρέπει να είναι όσο πιο φθηνό γίνεται, καθώς δέχεται thousands of beacons την ώρα. Στην πράξη χρησιμοποιώ ένα minimal handler που γράφει σε ClickHouse ή BigQuery. Ο σχεδιασμός σχήματος επηρεάζει πολύ την ταχύτητα queries αργότερα.

Ελάχιστο schema

CREATE TABLE rum_events (
  ts          DateTime64(3),
  metric      LowCardinality(String),  -- CLS | INP | LCP | FCP | TTFB
  value       Float32,
  rating      LowCardinality(String),  -- good | needs-improvement | poor
  url_path    String,
  device_type LowCardinality(String),  -- mobile | desktop | tablet
  effective_connection_type LowCardinality(String),
  navigation_type LowCardinality(String),
  session_id  String,
  country     LowCardinality(String)
) ENGINE = MergeTree()
PARTITION BY toDate(ts)
ORDER BY (metric, url_path, ts);

Το LowCardinality είναι κρίσιμο σε ClickHouse για στήλες με λίγες διακριτές τιμές. Μειώνει δραστικά το storage και επιταχύνει group-by queries.

Παράγωγη device_type

Μην εμπιστευτείς το User-Agent από JavaScript. Το UA σπάει σε browsers με privacy mode. Αντί αυτού, χρησιμοποίησε Client Hints (Sec-CH-UA-Mobile) στο server και γράψε το device_type εκεί.

Ανάλυση στο p75 ανά συσκευή

Το Google αξιολογεί τον ιστότοπό σου στο 75ο εκατοστημόριο των μετρήσεων, δηλαδή το 75% των χρηστών έχουν τιμή καλύτερη ή ίση από αυτήν. Ποτέ μη χρησιμοποιείς μέσο όρο. Ένα single 30s INP outlier μπορεί να ανεβάσει τον average στα κόκκινα, ενώ ο p75 είναι μια χαρά.

Query για p75 INP ανά route και συσκευή

SELECT
  url_path,
  device_type,
  quantileExact(0.75)(value) AS p75_inp,
  count() AS samples
FROM rum_events
WHERE metric = 'INP'
  AND ts > now() - INTERVAL 7 DAY
GROUP BY url_path, device_type
HAVING samples >= 100
ORDER BY p75_inp DESC
LIMIT 20;

Το HAVING samples >= 100 είναι κρίσιμο: p75 σε 5 measurements δεν σημαίνει τίποτα. Στατιστικά, χρειάζεσαι περίπου 1000 samples ανά bucket για σταθερό p75 με ±5% confidence.

Thresholds CWV 2026

  • LCP καλό: ≤ 2.5s, κακό: > 4.0s
  • INP καλό: ≤ 200ms, κακό: > 500ms
  • CLS καλό: ≤ 0.1, κακό: > 0.25

Attribution mode για διάγνωση INP

Από τη v3.5+, η βιβλιοθήκη web-vitals προσφέρει μια ξεχωριστή entry: web-vitals/attribution. Αντί για ένα απλό value, παίρνεις πληροφορίες για το τι προκάλεσε το πρόβλημα. Στο INP, για παράδειγμα, παίρνεις το event type, το CSS selector του target, και τη διάρκεια του longest input handler.

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

onINP((metric) => {
  const attr = metric.attribution;
  navigator.sendBeacon('/api/rum', JSON.stringify({
    name: 'INP',
    value: metric.value,
    rating: metric.rating,
    eventType: attr.interactionType,        // 'pointer' | 'keyboard'
    eventTarget: attr.interactionTarget,    // CSS selector
    inputDelay: attr.inputDelay,
    processingDuration: attr.processingDuration,
    presentationDelay: attr.presentationDelay,
    longAnimationFrameEntries: attr.longAnimationFrameEntries?.length,
  }));
});

Σε ένα recent audit, ένα e-commerce είχε p75 INP 480ms (οριακά «poor»). Το attribution έδειξε ότι το 70% των κακών INP events προέρχονταν από ένα συγκεκριμένο .add-to-cart button που έτρεχε ένα synchronous tracking script. Το να βρω αυτό μόνο με Lighthouse θα ήταν αδύνατο. Για περισσότερες τεχνικές βλέπε και τον οδηγό για βελτιστοποίηση LCP που δείχνει αντίστοιχη attribution για το LCP image.

bfcache, navigationType και prerender

Το πεδίο navigationType είναι από τα πιο underused features της βιβλιοθήκης. Παίρνει μία από τις τιμές: navigate, reload, back-forward, back-forward-cache, prerender, restore.

Γιατί έχει σημασία; Στα bfcache restores, το LCP είναι σχεδόν πάντα κάτω από 100ms, γιατί η σελίδα δεν φορτώνει ξανά από το δίκτυο. Αν αναμείξεις αυτά τα events με cold loads, θα παραμορφώσεις τα στατιστικά σου. Πάντα segment-άρε queries ανά navigation_type.

Παραδείγματα segmentation

-- LCP μόνο για cold navigations
SELECT quantileExact(0.75)(value)
FROM rum_events
WHERE metric = 'LCP'
  AND navigation_type IN ('navigate', 'reload')
  AND ts > now() - INTERVAL 7 DAY;

Επίσης σημαντικό: από το Chrome 121+, τα prerender events έχουν εξαιρετικά καλό LCP. Αν το site σου χρησιμοποιεί <script type="speculationrules">, θα δεις τις τιμές να πέφτουν δραματικά. Δες την αναλυτική περιγραφή στο README της web-vitals για το πώς αναφέρονται οι μετρικές.

Συνηθισμένα λάθη στο RUM setup

Από εκατοντάδες RUM implementations που έχω review-άρει, αυτά είναι τα top λάθη που σαμποτάρουν τα δεδομένα:

1. Sampling χωρίς weight

Πολλοί κάνουν 10% client-side sampling για να μειώσουν cost. Σωστά. Όμως μετά υπολογίζουν p75 στα δείγματα χωρίς να γράψουν το sampling rate στο event. Όταν αλλάξουν το rate σε 5%, τα παλιά queries δείχνουν αλλιώτικα νούμερα και χάνεις comparability. Πάντα κατέγραψε το sampling rate ως πεδίο.

2. Συλλογή σε admin/internal pages

Αν συλλέγεις CWV από το CMS dashboard ή το admin panel σου, μολύνεις το dataset με slow-but-irrelevant routes. Πρόσθεσε ένα route filter, π.χ. exclude /admin/*, /preview/*.

3. Μη χρήση του id field

Το metric.id είναι μοναδικό ανά μέτρηση. Αν δεν το στέλνεις, στο back-end δεν μπορείς να κάνεις deduplicate beacons που έφτασαν διπλά (συμβαίνει σε bad network conditions). Πάντα στείλε το id και κάνε dedupe στο write path.

4. CSP χωρίς connect-src

Αν έχεις strict Content Security Policy, βεβαιώσου ότι το RUM endpoint είναι στη connect-src directive, αλλιώς το sendBeacon θα αποτυγχάνει σιωπηλά.

5. Αγνόηση CLS shifts μετά το load

Πολλά CLS issues εμφανίζονται μετά από user interaction (π.χ. dropdown που σπρώχνει content). Αν φύγεις από το page πριν την οριστικοποίηση, χάνεις το CLS. Το visibilitychange flushing που έδειξα παραπάνω είναι ο μόνος τρόπος. Παρόμοιες τεχνικές υπάρχουν στον οδηγό για CLS optimization.

Συχνές ερωτήσεις

Ποια είναι η διαφορά μεταξύ field data και lab data;

Field data (RUM, CrUX) προέρχονται από πραγματικούς χρήστες, με τα δικά τους δίκτυα και συσκευές. Lab data (Lighthouse, WebPageTest) προέρχονται από synthetic runs σε ελεγχόμενο περιβάλλον. Το Google Search χρησιμοποιεί μόνο field data (CrUX) για ranking, οπότε το RUM είναι αυτό που πρέπει να βελτιστοποιείς.

Πρέπει να αντικαταστήσω το FID με το INP στο RUM μου;

Ναι. Από τις 12 Μαρτίου 2024 το FID αφαιρέθηκε από τα Core Web Vitals και αντικαταστάθηκε από το INP. Η βιβλιοθήκη web-vitals v4 δεν εκθέτει πλέον onFID. Αν τρέχεις παλιό κώδικα, χάνεις την επίσημη μέτρηση responsiveness που χρησιμοποιεί το Google Search.

Πόση κίνηση χρειάζομαι για αξιόπιστα p75 νούμερα;

Στατιστικά, χρειάζεσαι περίπου 1000 measurements ανά bucket (π.χ. ανά route και device class) για p75 με ±5% confidence interval. Με λιγότερα από 100 samples ανά bucket, μην εμπιστευτείς το αποτέλεσμα. Αύξησε το rolling window από 1 μέρα σε 7 ή 28 μέρες.

Πώς διαχωρίζω cold loads από bfcache restores;

Χρησιμοποίησε το πεδίο navigationType που εκθέτει η web-vitals. Οι τιμές navigate και reload είναι cold/warm loads, ενώ back-forward-cache είναι bfcache restore. Το LCP σε bfcache είναι σχεδόν πάντα κάτω από 100ms και θα παραμορφώσει τα νούμερά σου αν δεν τα διαχωρίσεις.

Επηρεάζει η βιβλιοθήκη web-vitals την απόδοση;

Το impact είναι αμελητέο. Η βιβλιοθήκη είναι ~2KB gzipped, χρησιμοποιεί native PerformanceObservers (όχι polling) και στέλνει beacons μόνο όταν η σελίδα κρύβεται. Σε δοκιμές μου, το INP overhead είναι κάτω από 1ms. Φόρτωσέ την με defer ή dynamic import για να μην επηρεάσει το LCP.

Μπορώ να χρησιμοποιήσω RUM χωρίς να στήσω back-end;

Ναι, μέσω SaaS RUM providers όπως SpeedCurve, DebugBear, ή ο RUM agent του Cloudflare. Το trade-off είναι λιγότερος έλεγχος στο schema και κόστος ανά event. Για production με >10M monthly page views, το self-hosted setup με ClickHouse συνήθως κοστίζει λιγότερο και δίνει πλήρη ευελιξία στο querying.

Nadia El-Sayed
Σχετικά με τον Συγγραφέα Nadia El-Sayed

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