The web vitals npm package is Google's official, dependency-free library for measuring Core Web Vitals — LCP, INP, and CLS — from real users, and deployed carefully it adds essentially zero security risk to your frontend. The "deployed carefully" clause is what this guide is about. Performance monitoring is one of the most common reasons teams inject third-party scripts, widen their CSP, and ship user data to endpoints nobody reviewed. The library itself is exemplary; the pipelines built around it are where the risk creeps in.
What the package is
web-vitals is maintained by the Google Chrome team (GitHub: GoogleChrome/web-vitals). It wraps the messy platform APIs — PerformanceObserver, layout shift entries, event timing — behind one function per metric, and encodes all the tricky measurement rules: when a metric is final, how back/forward cache restores are handled, which sessions count. It is tiny (on the order of 2 KB brotlied for the standard build) and has zero runtime dependencies, which from a supply chain standpoint is the best sentence in this whole review.
import { onCLS, onINP, onLCP, onTTFB, onFCP } from "web-vitals";
function report(metric) {
const body = JSON.stringify({
name: metric.name, // "LCP" | "INP" | "CLS" | ...
value: metric.value,
rating: metric.rating, // "good" | "needs-improvement" | "poor"
id: metric.id,
navigationType: metric.navigationType,
});
navigator.sendBeacon("/rum", body);
}
onCLS(report);
onINP(report);
onLCP(report);
onTTFB(report);
onFCP(report);
One historical note to save you confusion in older tutorials: FID (First Input Delay) was replaced by INP as the responsiveness Core Web Vital on March 12, 2024, and current versions of npm web-vitals center on onINP. Version 4 also dropped the old "base+polyfill" build that existed to support legacy Safari FID measurement, and added rich attribution data (input delay, processing duration, presentation delay) for diagnosing slow interactions. If your codebase still calls onFID, that is a sign the integration has not been touched in a while — worth a refresh.
Why field measurement needs a library at all
Lab tools (Lighthouse, WebPageTest) run in synthetic conditions; Core Web Vitals as Google evaluates them come from real Chrome users. Collecting your own field data — real user monitoring — is how you see the P75 on actual devices and networks, segment by page type or country, and catch regressions between releases. Getting the raw platform APIs right by hand is genuinely difficult (CLS session windows and bfcache behavior are full of edge cases), so a first-party library that encodes Chrome's own measurement logic is the correct buy-over-build.
The supply chain question: npm bundle vs third-party tag
There are two ways this library reaches your page, and they are not equivalent.
Bundle it from npm (recommended). npm install web-vitals, import it, ship it in your own bundle. The version is pinned in your lockfile, it executes from your origin, your CSP does not widen, and every upgrade flows through your normal review and SCA scanning like any other dependency. With npm package provenance you can also verify a release was built from the public GoogleChrome repository.
Load it from a CDN script tag. Convenient for static sites, but now a third-party origin can serve executable code into your page forever after. If you must, pin an exact version in the URL and add Subresource Integrity so the browser rejects any content change:
<script type="module">
import { onLCP } from "https://unpkg.com/web-vitals@4/dist/web-vitals.js";
// prefer self-hosting this file instead
</script>
The broader point applies to your whole RUM stack: commercial performance monitors are delivered as tags, and each tag is an unreviewed code-execution grant. A monitoring vendor compromise is a Magecart-style scenario. Favor the npm-bundled path for collection and keep third-party execution off sensitive pages (checkout, account settings) even when you accept it elsewhere.
Designing the beacon pipeline like an endpoint, because it is one
The collection endpoint receiving your metrics is an unauthenticated public API that accepts writes from every visitor. Treat it accordingly:
- Minimize the payload. Metric name, value, rating, id, page path (not full URL — query strings leak tokens and PII with astonishing regularity), and coarse context like navigation type. Resist attaching user IDs; if you need session correlation, use a random per-pageload ID like the
metric.idthe library already gives you. - Validate server-side. Names against an allowlist, values against sane numeric bounds, body size capped. Anything can POST to this endpoint; your dashboards should not be scriptable by a curl loop, and your analytics store should not accept 10 MB "metrics."
- Rate-limit and don't trust it for decisions. RUM data is attacker-influenceable by construction. It is fine for aggregate percentiles; it is not an input to anything security-relevant.
- Use
sendBeaconorfetchwithkeepalive. This is a reliability point rather than security, but it determines whether you actually receive metrics from unloading pages, and it keeps you from hacking around lifecycle events with synchronous XHR.
CSP interaction is pleasantly minimal when self-bundled: the script is same-origin, and the beacon needs only your own connect-src. That "no CSP changes required" property is a real, underrated argument for the npm path — every host added to a CSP is a permanent widening of your XSS blast radius.
Operational tips that keep the data useful
A few field-tested practices: report all metrics, not just final page-unload values, since INP and CLS update over the page lifetime and the library calls your handler as values change (deduplicate by metric.id server-side, keeping the last value). Segment by navigationType — bfcache restores and prerenders have very different profiles and will skew your percentiles if mixed silently. And wire your RUM percentiles into release monitoring: a CLS regression showing up an hour after a deploy is a rollback trigger exactly like an error-rate spike. Teams that run continuous scanning on every release usually find it natural to gate on performance budgets in the same pipeline.
Review verdict
This is about as good as a frontend dependency gets: first-party maintainers with unique insight into the metrics, zero dependencies, tiny size, active releases tracking the evolving Core Web Vitals definitions. The risk lives in deployment choices — third-party tags instead of bundling, over-collection in beacons, unvalidated collection endpoints — all of which are yours to control. Bundle it, minimize the beacon, validate the endpoint, and you get production performance visibility with a security cost that rounds to zero.
FAQ
Is the web-vitals npm package safe to add?
Yes. It is maintained by the Google Chrome team, has zero runtime dependencies, no recorded CVEs, and a tiny footprint. The main risks in RUM setups come from loading monitoring code via third-party tags and from over-collecting data in beacons, not from this library.
Should I load web-vitals from a CDN or bundle it?
Bundle it from npm. That pins the version in your lockfile, keeps execution on your origin, avoids CSP changes, and puts upgrades through your normal dependency scanning. If you must use a CDN tag, pin an exact version and add Subresource Integrity, or better, self-host the file.
Does web-vitals still measure FID?
INP replaced FID as the responsiveness Core Web Vital in March 2024, and current versions of the library focus on onINP. Code still wiring up onFID is out of date and should migrate to INP with the v4+ attribution build for diagnosis.
Can collecting Core Web Vitals leak user data?
It can if you attach full URLs, user identifiers, or free-form context to beacons. Send metric name, value, rating, and page path only; strip query strings; and treat the collection endpoint as an untrusted public API with validation and rate limits.