跪拜 Guibai
← All articles
Frontend

Why Your RUM Data Is Wrong: The 6 Traps of Performance Monitoring

By 前端阿凡 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

Optimizing against wrong metrics wastes months, and most self-built SDKs get at least one metric wrong. This article quantifies every common mistake and provides a testable architecture that makes your performance data defensible—a practical baseline for any team building its own RUM.

Summary

This article tackles the final—and most deceptive—front of performance: knowing whether you really got faster. Using only the Node.js standard library, the author builds a complete RUM (Real User Monitoring) stack and benchmarks why common misconceptions fail. For example, summing layout shifts inflates p75 CLS by 2.68x, mixing mobile 1% and desktop 10% samples cuts true p75 by −30.8%, and averaging p90s across shards is systematically biased while summing histograms is not.

The solution hinges on a design principle: write metric definitions as pure functions so they can be tested, replayed from real entry logs, and shared between lab and field. The article then walks through implementing CLS session windows, LCP freeze on first input, INP trimming, weighted histograms, byte-aware beacon batching, and buffer eviction policies. Every claim is backed by a benchmark script that produces the cited numbers, plus a 56-assertion test suite that locks each definition to its correct behavior.

Takeaways
— CLS naive sum inflates p75 by 2.68x and drops the rate of sessions rated 'good' from 26.2% to 2.2%.
— Mixing unweighted samples from different rates (1% mobile, 10% desktop) biases p75 by −30.8%; weighting corrects it to −2.9%.
— Averaging p90s across shards gives −1.28% error, while summing 50ms-bucket histograms gives +0.20%.
— Reservoir sampling at n=100 yields average p75 error of 6.89%; n=2000 still 1.05%, costing 20x storage.
— INP trims the top 1 out of every 50 interactions; 4 spikes of 480ms in 200 interactions disappear, only the 5th shows.
— JSON.stringify of 10k metric entries takes ~2.6ms, itself a long task; per-entry collection is sub-microsecond.
— sendBeacon silently drops payloads over 64KB; 500 rows with attribution reach 83513B, so budget must be in bytes.
— Metric definitions must be pure functions to enable Node-based assertion tests and real entry replay.
Conclusions

Most self-built SDKs implement CLS as a sum, a systematic error that inflates p75 by 2.68x on realistic pages.

The non-invertibility of quantiles means any aggregate that loses distribution shape is inherently lossy; histograms are the only additive approximation.

The observer effect is more about serialization and buffer management than the act of measuring; monitoring can become a self-induced long task.

The deliberate trimming of INP spike outliers reveals a design tension: metrics optimize for reproducibility over pathology, so rare extreme events need separate tracking.

Concepts & terms
CLS session window
Layout shifts are grouped into windows with a >1s gap and a 5s maximum; CLS is the sum of the window with the highest total, not the sum of all shifts.
INP trimming
Interaction to Next Paint (INP) drops the highest 1 out of every 50 interactions (98th percentile) to measure 'typical worst', ignoring rare spikes unless they become systematic.
Percentile (p75)
The 75th percentile value, used by Core Web Vitals as the primary performance threshold; it must be derived from an additive representation like a histogram, never by averaging pre-computed percentiles.
sendBeacon 64KB limit
Browsers silently discard beacon payloads over ~64KB without throwing or retrying, so reporting must compute byte size before every flush and split batches.
Columnar encoding
A compression method that stores field names once and values as columns, reducing payload size compared to JSON; it requires explicit handling of optional columns to avoid silently dropping data.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗