# In-house RUM without a service: what it really takes

One beacon weighs 239 bytes and twenty samples in a batch fall to 273 bytes with gzip. PerformanceObserver and sendBeacon are enough, no service.

> Page: https://ui.eneawork.it/blog/performance-rum-implementazione
> Markdown: https://ui.eneawork.it/blog/performance-rum-implementazione.md

*2026-11-01*

Un RUM in casa si costruisce con due API del browser, PerformanceObserver e navigator.sendBeacon, e un endpoint che riceve JSON. Il costo di rete non è il problema: un campione pesa 239 byte e venti campioni in un lotto compresso stanno in 273 byte.

Payload misurato con node e zlib su Windows, il 1 novembre 2026: un campione con path, TTFB, LCP, CLS, INP, larghezza viewport e tipo di connessione pesa 239 byte. Un lotto di venti campioni: 4.807 byte in JSON, 273 byte dopo gzip, rapporto 17,6 a 1. Su 1.000 visite il totale passa da 235 KB a 13 KB. Il catalogo EneUI misura 111.985 componenti, 1.556 blocchi e 12.808 icone da 1.472 librerie, letti da /api/stats il 1 novembre 2026. Metriche: web.dev, CC BY 4.0.

## Cosa raccogliere, e quando

## Il collector minimo

const q = []; const push = (kind, value) => q.push({ kind, value, path: location.pathname }); new PerformanceObserver((l) => { for (const e of l.getEntries()) push("lcp", Math.round(e.startTime)); }).observe({ type: "largest-contentful-paint", buffered: true }); new PerformanceObserver((l) => { for (const e of l.getEntries()) if (!e.hadRecentInput) push("cls", +e.value.toFixed(4)); }).observe({ type: "layout-shift", buffered: true }); addEventListener("pagehide", () => { const nav = performance.getEntriesByType("navigation")[0]; push("ttfb", Math.round(nav.responseStart)); navigator.sendBeacon("/rum", new Blob([JSON.stringify(q)], { type: "application/json" })); });

Il codice richiede un endpoint /rum che accetti POST JSON e un browser con sendBeacon. Il vantaggio di sendBeacon è che la richiesta sopravvive alla chiusura della pagina, dove fetch verrebbe annullata. L'evento pagehide copre anche il passaggio in bfcache, dove unload non arriva.

## Quanto pesa davvero

node -e "const z=require('zlib');const b=JSON.stringify(Array.from({length:20},(_,i)=>({kind:'lcp',value:1200+i*20})));console.log(Buffer.byteLength(b),z.gzipSync(b).length)"

Il comando richiede Node e nessuna dipendenza. Su un lotto reale i valori cambiano con il numero di campioni, ma l'ordine di grandezza resta: pochi kilobyte per visita, ridotti a poche centinaia di byte quando il server comprime la richiesta. Se il tuo endpoint non comprime, aggiungi Content-Encoding: gzip e accetta il corpo compresso: il risparmio è di un fattore 17.

## Dal campione al numero pubblicabile

Un RUM che pubblica la media mente: la media nasconde la coda lenta. Si ordina il campione per metrica e si prende il valore al 75° percentile, che è il numero usato da CrUX e da Search Console. Con venti campioni il 75° percentile è il quindicesimo valore in ordine crescente, un dato troppo rumoroso per una decisione: serve un volume di visite sufficiente, altrimenti si legge il dato come indicazione e non come verdetto.

## Il limite da dichiarare

Chi blocca gli script non invia nulla, quindi il campione è distorto verso chi ha un browser pulito e una connessione decente. Le metriche perse per crash o chiusura brusca non arrivano. E un beacon che contiene il path è un dato personale: serve una base giuridica, la minimizzazione dei campi e una retention breve, non un foglio di calcolo eterno.

## Dove continuare

Il catalogo EneUI indicizza 111.985 componenti, il blocco banner 01 e l'icona chart bar, quella che finisce nel pannello. Per la meccanica degli osservatori leggi PerformanceObserver in pratica, per il confronto tra laboratorio e campo misurare l'LCP, per le soglie cosa sono i Core Web Vitals, e per il confronto tra architetture framework frontend a confronto.

## FAQ

### Serve un servizio esterno per il RUM?

No. PerformanceObserver raccoglie le metriche nel browser e sendBeacon le invia a un endpoint tuo. Un servizio esterno aggiunge dashboard e confronti, che puoi ricostruire con una query.

### Perché inviare su pagehide e non su unload?

Perché unload non arriva quando la pagina entra in bfcache, e su mobile molti browser lo saltano. pagehide copre entrambi i casi e sendBeacon sopravvive alla chiusura.

### Quanti campioni servono per un 75° percentile affidabile?

Almeno qualche centinaio di visite per pagina e per tipo di dispositivo. Con venti campioni il percentile balla di centinaia di millisecondi e non regge una decisione.

### Il beacon RUM rallenta la pagina?

No, se parte su pagehide e resta sotto il kilobyte compresso. Il costo vero è lato server: scrivere una riga per visita e aggregare i percentili con una query pianificata.

### Come tratto i dati personali nel beacon?

Raccogli solo ciò che serve: path, tipo di dispositivo, metriche. Niente identificatori stabili, retention breve e informativa nell'informativa privacy. Il path può identificare una persona su pagine rare.
