# `web-vitals` - [Overview](#overview) - [Install and load the library](#installation) - [From npm](#import-web-vitals-from-npm) - [From a CDN](#load-web-vitals-from-a-cdn) - [Usage](#usage) - [Basic usage](#basic-usage) - [Report the value on every change](#report-the-value-on-every-change) - [Report only the delta of changes](#report-only-the-delta-of-changes) - [Send the results to an analytics endpoint](#send-the-results-to-an-analytics-endpoint) - [Send the results to Google Analytics](#send-the-results-to-google-analytics) - [Send the results to Google Tag Manager](#send-the-results-to-google-tag-manager) - [Send attribution data](#send-attribution-data) - [Batch multiple reports together](#batch-multiple-reports-together) - [Build options](#build-options) - [Which build is right for you?](#which-build-is-right-for-you) - [API](#api) - [Types](#types) - [Functions](#functions) - [Rating Thresholds](#rating-thresholds) - [Attribution](#attribution) - [Browser Support](#browser-support) - [Limitations](#limitations) - [Development](#development) - [Integrations](#integrations) - [License](#license) ## Overview The `web-vitals` library is a tiny (~2K, brotli'd), modular library for measuring all the [Web Vitals](https://web.dev/articles/vitals) metrics on real users, in a way that accurately matches how they're measured by Chrome and reported to other Google tools (e.g. [Chrome User Experience Report](https://developers.google.com/web/tools/chrome-user-experience-report), [Page Speed Insights](https://developers.google.com/speed/pagespeed/insights/), [Search Console's Speed Report](https://webmasters.googleblog.com/2019/11/search-console-speed-report.html)). The library supports all of the [Core Web Vitals](https://web.dev/articles/vitals#core_web_vitals) as well as a number of other metrics that are useful in diagnosing [real-user](https://web.dev/articles/user-centric-performance-metrics) performance issues. ### Core Web Vitals - [Cumulative Layout Shift (CLS)](https://web.dev/articles/cls) - [Interaction to Next Paint (INP)](https://web.dev/articles/inp) - [Largest Contentful Paint (LCP)](https://web.dev/articles/lcp) ### Other metrics - [First Contentful Paint (FCP)](https://web.dev/articles/fcp) - [Time to First Byte (TTFB)](https://web.dev/articles/ttfb) - [First Input Delay (FID)](https://web.dev/articles/fid) > [!CAUTION] > FID is deprecated and will be removed in the next major release. ## Install and load the library The `web-vitals` library uses the `buffered` flag for [PerformanceObserver](https://developer.mozilla.org/docs/Web/API/PerformanceObserver/observe), allowing it to access performance entries that occurred before the library was loaded. This means you do not need to load this library early in order to get accurate performance data. In general, this library should be deferred until after other user-impacting code has loaded. ### From npm You can install this library from npm by running: ```sh npm install web-vitals ``` > [!NOTE] > If you're not using npm, you can still load `web-vitals` via ` ``` **Load the "standard" build** _(using a classic script)_ ```html ``` **Load the "attribution" build** _(using a module script)_ ```html ``` **Load the "attribution" build** _(using a classic script)_ ```html ``` ## Usage ### Basic usage Each of the Web Vitals metrics is exposed as a single function that takes a `callback` function that will be called any time the metric value is available and ready to be reported. The following example measures each of the Core Web Vitals metrics and logs the result to the console once its value is ready to report. _(The examples below import the "standard" build, but they will work with the "attribution" build as well.)_ ```js import {onCLS, onINP, onLCP} from 'web-vitals'; onCLS(console.log); onINP(console.log); onLCP(console.log); ``` Note that some of these metrics will not report until the user has interacted with the page, switched tabs, or the page starts to unload. If you don't see the values logged to the console immediately, try reloading the page (with [preserve log](https://developer.chrome.com/docs/devtools/console/reference/#persist) enabled) or switching tabs and then switching back. Also, in some cases a metric callback may never be called: - FID and INP are not reported if the user never interacts with the page. - CLS, FCP, FID, and LCP are not reported if the page was loaded in the background. In other cases, a metric callback may be called more than once: - CLS and INP should be reported any time the [page's `visibilityState` changes to hidden](https://developer.chrome.com/blog/page-lifecycle-api/#advice-hidden). - All metrics are reported again (with the above exceptions) after a page is restored from the [back/forward cache](https://web.dev/articles/bfcache). > [!WARNING] > Do not call any of the Web Vitals functions (e.g. `onCLS()`, `onINP()`, `onLCP()`) more than once per page load. Each of these functions creates a `PerformanceObserver` instance and registers event listeners for the lifetime of the page. While the overhead of calling these functions once is negligible, calling them repeatedly on the same page may eventually result in a memory leak. ### Report the value on every change In most cases, you only want the `callback` function to be called when the metric is ready to be reported. However, it is possible to report every change (e.g. each larger layout shift as it happens) by setting `reportAllChanges` to `true` in the optional, [configuration object](#reportopts) (second parameter). > [!IMPORTANT] > `reportAllChanges` only reports when the **metric changes**, not for each **input to the metric**. For example, a new layout shift that does not increase the CLS metric will not be reported even with `reportAllChanges` set to `true` because the CLS metric has not changed. Similarly, for INP, each interaction is not reported even with `reportAllChanges` set to `true`—just when an interaction causes an increase to INP. This can be useful when debugging, but in general using `reportAllChanges` is not needed (or recommended) for measuring these metrics in production. ```js import {onCLS} from 'web-vitals'; // Logs CLS as the value changes. onCLS(console.log, {reportAllChanges: true}); ``` ### Report only the delta of changes Some analytics providers allow you to update the value of a metric, even after you've already sent it to their servers (overwriting the previously-sent value with the same `id`). Other analytics providers, however, do not allow this, so instead of reporting the new value, you need to report only the delta (the difference between the current value and the last-reported value). You can then compute the total value by summing all metric deltas sent with the same ID. The following example shows how to use the `id` and `delta` properties: ```js import {onCLS, onINP, onLCP} from 'web-vitals'; function logDelta({name, id, delta}) { console.log(`${name} matching ID ${id} changed by ${delta}`); } onCLS(logDelta); onINP(logDelta); onLCP(logDelta); ``` > [!NOTE] > The first time the `callback` function is called, its `value` and `delta` properties will be the same. In addition to using the `id` field to group multiple deltas for the same metric, it can also be used to differentiate different metrics reported on the same page. For example, after a back/forward cache restore, a new metric object is created with a new `id` (since back/forward cache restores are considered separate page visits). ### Send the results to an analytics endpoint The following example measures each of the Core Web Vitals metrics and reports them to a hypothetical `/analytics` endpoint, as soon as each is ready to be sent. The `sendToAnalytics()` function uses the [`navigator.sendBeacon()`](https://developer.mozilla.org/docs/Web/API/Navigator/sendBeacon) method (if available), but falls back to the [`fetch()`](https://developer.mozilla.org/docs/Web/API/Fetch_API) API when not. ```js import {onCLS, onINP, onLCP} from 'web-vitals'; function sendToAnalytics(metric) { // Replace with whatever serialization method you prefer. // Note: JSON.stringify will likely include more data than you need. const body = JSON.stringify(metric); // Use `navigator.sendBeacon()` if available, falling back to `fetch()`. (navigator.sendBeacon && navigator.sendBeacon('/analytics', body)) || fetch('/analytics', {body, method: 'POST', keepalive: true}); } onCLS(sendToAnalytics); onINP(sendToAnalytics); onLCP(sendToAnalytics); ``` ### Send the results to Google Analytics Google Analytics does not support reporting metric distributions in any of its built-in reports; however, if you set a unique event parameter value (in this case, the metric_id, as shown in the example below) on every metric instance that you send to Google Analytics, you can create a report yourself by first getting the data via the [Google Analytics Data API](https://developers.google.com/analytics/devguides/reporting/data/v1) or via [BigQuery export](https://support.google.com/analytics/answer/9358801) and then visualizing it any charting library you choose. [Google Analytics 4](https://support.google.com/analytics/answer/10089681) introduces a new Event model allowing custom parameters instead of a fixed category, action, and label. It also supports non-integer values, making it easier to measure Web Vitals metrics compared to previous versions. ```js import {onCLS, onINP, onLCP} from 'web-vitals'; function sendToGoogleAnalytics({name, delta, value, id}) { // Assumes the global `gtag()` function exists, see: // https://developers.google.com/analytics/devguides/collection/ga4 gtag('event', name, { // Built-in params: value: delta, // Use `delta` so the value can be summed. // Custom params: metric_id: id, // Needed to aggregate events. metric_value: value, // Optional. metric_delta: delta, // Optional. // OPTIONAL: any additional params or debug info here. // See: https://web.dev/articles/debug-performance-in-the-field // metric_rating: 'good' | 'needs-improvement' | 'poor', // debug_info: '...', // ... }); } onCLS(sendToGoogleAnalytics); onINP(sendToGoogleAnalytics); onLCP(sendToGoogleAnalytics); ``` For details on how to query this data in [BigQuery](https://cloud.google.com/bigquery), or visualise it in [Looker Studio](https://lookerstudio.google.com/), see [Measure and debug performance with Google Analytics 4 and BigQuery](https://web.dev/articles/vitals-ga4). ### Send the results to Google Tag Manager While `web-vitals` can be called directly from Google Tag Manager, using a pre-defined custom template makes this considerably easier. Some recommended templates include: - [Core Web Vitals](https://tagmanager.google.com/gallery/#/owners/gtm-templates-simo-ahava/templates/core-web-vitals) by [Simo Ahava](https://www.simoahava.com/). See [Track Core Web Vitals in GA4 with Google Tag Manager](https://www.simoahava.com/analytics/track-core-web-vitals-in-ga4-with-google-tag-manager/) for usage and installation instructions. - [Web Vitals Template for Google Tag Manager](https://github.com/google-marketing-solutions/web-vitals-gtm-template) by The Google Marketing Solutions team. See the [README](https://github.com/google-marketing-solutions/web-vitals-gtm-template?tab=readme-ov-file#web-vitals-template-for-google-tag-manager) for usage and installation instructions. ### Send attribution data When using the [attribution build](#attribution-build), you can send additional data to help you debug _why_ the metric values are they way they are. This example sends an additional `debug_target` param to Google Analytics, corresponding to the element most associated with each metric. ```js import {onCLS, onINP, onLCP} from 'web-vitals/attribution'; function sendToGoogleAnalytics({name, delta, value, id, attribution}) { const eventParams = { // Built-in params: value: delta, // Use `delta` so the value can be summed. // Custom params: metric_id: id, // Needed to aggregate events. metric_value: value, // Optional. metric_delta: delta, // Optional. }; switch (name) { case 'CLS': eventParams.debug_target = attribution.largestShiftTarget; break; case 'INP': eventParams.debug_target = attribution.interactionTarget; break; case 'LCP': eventParams.debug_target = attribution.element; break; } // Assumes the global `gtag()` function exists, see: // https://developers.google.com/analytics/devguides/collection/ga4 gtag('event', name, eventParams); } onCLS(sendToGoogleAnalytics); onINP(sendToGoogleAnalytics); onLCP(sendToGoogleAnalytics); ``` > [!NOTE] > This example relies on custom [event parameters](https://support.google.com/analytics/answer/11396839) in Google Analytics 4. See [Debug performance in the field](https://web.dev/articles/debug-performance-in-the-field) for more information and examples. ### Batch multiple reports together Rather than reporting each individual Web Vitals metric separately, you can minimize your network usage by batching multiple metric reports together in a single network request. However, since not all Web Vitals metrics become available at the same time, and since not all metrics are reported on every page, you cannot simply defer reporting until all metrics are available. Instead, you should keep a queue of all metrics that were reported and flush the queue whenever the page is backgrounded or unloaded: ```js import {onCLS, onINP, onLCP} from 'web-vitals'; const queue = new Set(); function addToQueue(metric) { queue.add(metric); } function flushQueue() { if (queue.size > 0) { // Replace with whatever serialization method you prefer. // Note: JSON.stringify will likely include more data than you need. const body = JSON.stringify([...queue]); // Use `navigator.sendBeacon()` if available, falling back to `fetch()`. (navigator.sendBeacon && navigator.sendBeacon('/analytics', body)) || fetch('/analytics', {body, method: 'POST', keepalive: true}); queue.clear(); } } onCLS(addToQueue); onINP(addToQueue); onLCP(addToQueue); // Report all available metrics whenever the page is backgrounded or unloaded. addEventListener('visibilitychange', () => { if (document.visibilityState === 'hidden') { flushQueue(); } }); ``` > [!NOTE] > See [the Page Lifecycle guide](https://developers.google.com/web/updates/2018/07/page-lifecycle-api#legacy-lifecycle-apis-to-avoid) for an explanation of why `visibilitychange` is recommended over events like `beforeunload` and `unload`. ## Build options The `web-vitals` package includes both "standard" and "attribution" builds, as well as different formats of each to allow developers to choose the format that best meets their needs or integrates with their architecture. The following table lists all the builds distributed with the `web-vitals` package on npm.
Filename (all within dist/* )
|
Export | Description |
web-vitals.js |
pkg.module |
An ES module bundle of all metric functions, without any attribution features. This is the "standard" build and is the simplest way to consume this library out of the box. |
web-vitals.umd.cjs |
pkg.main |
A UMD version of the web-vitals.js bundle (exposed on the self.webVitals.* namespace).
|
web-vitals.iife.js |
-- |
An IIFE version of the web-vitals.js bundle (exposed on the self.webVitals.* namespace).
|
web-vitals.attribution.js |
-- | An ES module version of all metric functions that includes attribution features. |
web-vitals.attribution.umd.cjs |
-- |
A UMD version of the web-vitals.attribution.js build (exposed on the self.webVitals.* namespace).
|
web-vitals.attribution.iife.js |
-- |
An IIFE version of the web-vitals.attribution.js build (exposed on the self.webVitals.* namespace).
|