Code Monkey home page Code Monkey logo

web-vitals's Introduction

web-vitals

Overview

The web-vitals library is a tiny (~1.5K, brotli'd), modular library for measuring all the Web 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, Page Speed Insights, Search Console's Speed Report).

The library supports all of the Core Web Vitals as well as a number of other metrics that are useful in diagnosing real-user performance issues.

Core Web Vitals

Other metrics

Install and load the library

The web-vitals library uses the buffered flag for PerformanceObserver, 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:

npm install web-vitals

Note: If you're not using npm, you can still load web-vitals via <script> tags from a CDN like unpkg.com. See the load web-vitals from a CDN usage example below for details.

There are a few different builds of the web-vitals library, and how you load the library depends on which build you want to use.

For details on the difference between the builds, see which build is right for you.

1. The "standard" build

To load the "standard" build, import modules from the web-vitals package in your application code (as you would with any npm package and node-based build tool):

import {onLCP, onFID, onCLS} from 'web-vitals';

onCLS(console.log);
onFID(console.log);
onLCP(console.log);

Note: in version 2, these functions were named getXXX() rather than onXXX(). They've been renamed in version 3 to reduce confusion (see #217 for details) and will continue to be available using the getXXX() until at least version 4. Users are encouraged to switch to the new names, though, for future compatibility.

2. The "attribution" build

Measuring the Web Vitals scores for your real users is a great first step toward optimizing the user experience. But if your scores aren't good, the next step is to understand why they're not good and work to improve them.

The "attribution" build helps you do that by including additional diagnostic information with each metric to help you identify the root cause of poor performance as well as prioritize the most important things to fix.

The "attribution" build is slightly larger than the "standard" build (by about 600 bytes, brotli'd), so while the code size is still small, it's only recommended if you're actually using these features.

To load the "attribution" build, change any import statements that reference web-vitals to web-vitals/attribution:

- import {onLCP, onFID, onCLS} from 'web-vitals';
+ import {onLCP, onFID, onCLS} from 'web-vitals/attribution';

Usage for each of the imported function is identical to the standard build, but when importing from the attribution build, the Metric object will contain an additional attribution property.

See Send attribution data for usage examples, and the attribution reference for details on what values are added for each metric.

3. The "base+polyfill" build

⚠️ Warning ⚠️ the "base+polyfill" build is deprecated. See #238 for details.

Loading the "base+polyfill" build is a two-step process:

First, in your application code, import the "base" build rather than the "standard" build. To do this, change any import statements that reference web-vitals to web-vitals/base:

- import {onLCP, onFID, onCLS} from 'web-vitals';
+ import {onLCP, onFID, onCLS} from 'web-vitals/base';

Then, inline the code from dist/polyfill.js into the <head> of your pages. This step is important since the "base" build will error if the polyfill code has not been added.

<!doctype html>
<html>
  <head>
    <script>
      // Inline code from `dist/polyfill.js` here
    </script>
  </head>
  <body>
    ...
  </body>
</html>

It's important that the code is inlined directly into the HTML. Do not link to an external script file, as that will negatively affect performance:

<!-- GOOD -->
<script>
  // Inline code from `dist/polyfill.js` here
</script>

<!-- BAD! DO NOT DO! -->
<script src="/path/to/polyfill.js"></script>

Also note that the code must go in the <head> of your pages in order to work. See how the polyfill works for more details.

Tip: while it's certainly possible to inline the code in dist/polyfill.js by copy and pasting it directly into your templates, it's better to automate this process in a build step—otherwise you risk the "base" and the "polyfill" scripts getting out of sync when new versions are released.

From a CDN

The recommended way to use the web-vitals package is to install it from npm and integrate it into your build process. However, if you're not using npm, it's still possible to use web-vitals by requesting it from a CDN that serves npm package files.

The following examples show how to load web-vitals from unpkg.com:

Important! The unpkg.com CDN is shown here for example purposes only. unpkg.com is not affiliated with Google, and there are no guarantees that the URLs shown in these examples will continue to work in the future.

Load the "standard" build (using a module script)

<!-- Append the `?module` param to load the module version of `web-vitals` -->
<script type="module">
  import {onCLS, onFID, onLCP} from 'https://unpkg.com/web-vitals@3?module';

  onCLS(console.log);
  onFID(console.log);
  onLCP(console.log);
</script>

Load the "standard" build (using a classic script)

<script>
  (function () {
    var script = document.createElement('script');
    script.src = 'https://unpkg.com/web-vitals@3/dist/web-vitals.iife.js';
    script.onload = function () {
      // When loading `web-vitals` using a classic script, all the public
      // methods can be found on the `webVitals` global namespace.
      webVitals.onCLS(console.log);
      webVitals.onFID(console.log);
      webVitals.onLCP(console.log);
    };
    document.head.appendChild(script);
  })();
</script>

Load the "attribution" build (using a module script)

<!-- Append the `?module` param to load the module version of `web-vitals` -->
<script type="module">
  import {
    onCLS,
    onFID,
    onLCP,
  } from 'https://unpkg.com/web-vitals@3/dist/web-vitals.attribution.js?module';

  onCLS(console.log);
  onFID(console.log);
  onLCP(console.log);
</script>

Load the "attribution" build (using a classic script)

<script>
  (function () {
    var script = document.createElement('script');
    script.src =
      'https://unpkg.com/web-vitals@3/dist/web-vitals.attribution.iife.js';
    script.onload = function () {
      // When loading `web-vitals` using a classic script, all the public
      // methods can be found on the `webVitals` global namespace.
      webVitals.onCLS(console.log);
      webVitals.onFID(console.log);
      webVitals.onLCP(console.log);
    };
    document.head.appendChild(script);
  })();
</script>

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.)

import {onCLS, onFID, onLCP} from 'web-vitals';

onCLS(console.log);
onFID(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 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:

Warning: do not call any of the Web Vitals functions (e.g. onCLS(), onFID(), 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 (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.

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:

import {onCLS, onFID, onLCP} from 'web-vitals';

function logDelta({name, id, delta}) {
  console.log(`${name} matching ID ${id} changed by ${delta}`);
}

onCLS(logDelta);
onFID(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() method (if available), but falls back to the fetch() API when not.

import {onCLS, onFID, 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);
onFID(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 or via BigQuery export and then visualizing it any charting library you choose.

Google Analytics 4 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.

import {onCLS, onFID, 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);
onFID(sendToGoogleAnalytics);
onLCP(sendToGoogleAnalytics);

For details on how to query this data in BigQuery, or visualise it in Looker Studio, see Measure and debug performance with Google Analytics 4 and BigQuery.

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:

Send attribution data

When using the 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.

import {onCLS, onFID, 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 'FID':
      eventParams.debug_target = attribution.eventTarget;
      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);
onFID(sendToGoogleAnalytics);
onLCP(sendToGoogleAnalytics);

Note: this example relies on custom event parameters in Google Analytics 4.

See 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:

import {onCLS, onFID, 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);
onFID(addToQueue);
onLCP(addToQueue);

// Report all available metrics whenever the page is backgrounded or unloaded.
addEventListener('visibilitychange', () => {
  if (document.visibilityState === 'hidden') {
    flushQueue();
  }
});

// NOTE: Safari does not reliably fire the `visibilitychange` event when the
// page is being unloaded. If Safari support is needed, you should also flush
// the queue in the `pagehide` event.
addEventListener('pagehide', flushQueue);

Note: see the Page Lifecycle guide for an explanation of why visibilitychange and pagehide are recommended over events like beforeunload and unload.

Build options

The web-vitals package includes builds for the "standard", "attribution", and "base+polyfill" (deprecated) 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 window.webVitals.* namespace).
web-vitals.iife.js -- An IIFE version of the web-vitals.js bundle (exposed on the window.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 window.webVitals.* namespace).
web-vitals.attribution.iife.js -- An IIFE version of the web-vitals.attribution.js build (exposed on the window.webVitals.* namespace).
web-vitals.base.js --

This build has been deprecated.

An ES module bundle containing just the "base" part of the "base+polyfill" version.

Use this bundle if (and only if) you've also added the polyfill.js script to the <head> of your pages. See how to use the polyfill for more details.
web-vitals.base.umd.cjs --

This build has been deprecated.

A UMD version of the web-vitals.base.js bundle (exposed on the window.webVitals.* namespace).

web-vitals.base.iife.js --

This build has been deprecated.

An IIFE version of the web-vitals.base.js bundle (exposed on the window.webVitals.* namespace).

polyfill.js --

This build has been deprecated.

The "polyfill" part of the "base+polyfill" version. This script should be used with either web-vitals.base.js, web-vitals.base.umd.cjs, or web-vitals.base.iife.js (it will not work with any script that doesn't have "base" in the filename).

See how to use the polyfill for more details.

Which build is right for you?

Most developers will generally want to use "standard" build (via either the ES module or UMD version, depending on your bundler/build system), as it's the easiest to use out of the box and integrate into existing tools.

However, if you'd lke to collect additional debug information to help you diagnose performance bottlenecks based on real-user issues, use the "attribution" build.

For guidance on how to collect and use real-user data to debug performance issues, see Debug performance in the field.

How the polyfill works

⚠️ Warning ⚠️ the "base+polyfill" build is deprecated. See #238 for details.

The polyfill.js script adds event listeners (to track FID cross-browser), and it records initial page visibility state as well as the timestamp of the first visibility change to hidden (to improve the accuracy of CLS, FCP, LCP, and FID). It also polyfills the Navigation Timing API Level 2 in browsers that only support the original (now deprecated) Navigation Timing API.

In order for the polyfill to work properly, the script must be the first script added to the page, and it must run before the browser renders any content to the screen. This is why it needs to be added to the <head> of the document.

The "standard" build of the web-vitals library includes some of the same logic found in polyfill.js. To avoid duplicating that code when using the "base+polyfill" build, the web-vitals.base.js bundle does not include any polyfill logic, instead it coordinates with the code in polyfill.js, which is why the two scripts must be used together.

API

Types:

Metric

interface Metric {
  /**
   * The name of the metric (in acronym form).
   */
  name: 'CLS' | 'FCP' | 'FID' | 'INP' | 'LCP' | 'TTFB';

  /**
   * The current value of the metric.
   */
  value: number;

  /**
   * The rating as to whether the metric value is within the "good",
   * "needs improvement", or "poor" thresholds of the metric.
   */
  rating: 'good' | 'needs-improvement' | 'poor';

  /**
   * The delta between the current value and the last-reported value.
   * On the first report, `delta` and `value` will always be the same.
   */
  delta: number;

  /**
   * A unique ID representing this particular metric instance. This ID can
   * be used by an analytics tool to dedupe multiple values sent for the same
   * metric instance, or to group multiple deltas together and calculate a
   * total. It can also be used to differentiate multiple different metric
   * instances sent from the same page, which can happen if the page is
   * restored from the back/forward cache (in that case new metrics object
   * get created).
   */
  id: string;

  /**
   * Any performance entries relevant to the metric value calculation.
   * The array may also be empty if the metric value was not based on any
   * entries (e.g. a CLS value of 0 given no layout shifts).
   */
  entries: (
    | PerformanceEntry
    | LayoutShift
    | FirstInputPolyfillEntry
    | NavigationTimingPolyfillEntry
  )[];

  /**
   * The type of navigation.
   *
   * This will be the value returned by the Navigation Timing API (or
   * `undefined` if the browser doesn't support that API), with the following
   * exceptions:
   * - 'back-forward-cache': for pages that are restored from the bfcache.
   * - 'back_forward' is renamed to 'back-forward' for consistency.
   * - 'prerender': for pages that were prerendered.
   * - 'restore': for pages that were discarded by the browser and then
   * restored by the user.
   */
  navigationType:
    | 'navigate'
    | 'reload'
    | 'back-forward'
    | 'back-forward-cache'
    | 'prerender'
    | 'restore';
}

Metric-specific subclasses:

MetricWithAttribution

See the attribution build section for details on how to use this feature.

interface MetricWithAttribution extends Metric {
  /**
   * An object containing potentially-helpful debugging information that
   * can be sent along with the metric value for the current page visit in
   * order to help identify issues happening to real-users in the field.
   */
  attribution: {[key: string]: unknown};
}

Metric-specific subclasses:

MetricRatingThresholds

The thresholds of metric's "good", "needs improvement", and "poor" ratings.

  • Metric values up to and including [0] are rated "good"
  • Metric values up to and including [1] are rated "needs improvement"
  • Metric values above [1] are "poor"
Metric value Rating
≦ [0] "good"
> [0] and ≦ [1] "needs improvement"
> [1] "poor"
export type MetricRatingThresholds = [number, number];

See also Rating Thresholds.

ReportCallback

interface ReportCallback {
  (metric: Metric): void;
}

Metric-specific subclasses:

ReportOpts

interface ReportOpts {
  reportAllChanges?: boolean;
  durationThreshold?: number;
}

LoadState

The LoadState type is used in several of the metric attribution objects.

/**
 * The loading state of the document. Note: this value is similar to
 * `document.readyState` but it subdivides the "interactive" state into the
 * time before and after the DOMContentLoaded event fires.
 *
 * State descriptions:
 * - `loading`: the initial document response has not yet been fully downloaded
 *   and parsed. This is equivalent to the corresponding `readyState` value.
 * - `dom-interactive`: the document has been fully loaded and parsed, but
 *   scripts may not have yet finished loading and executing.
 * - `dom-content-loaded`: the document is fully loaded and parsed, and all
 *   scripts (except `async` scripts) have loaded and finished executing.
 * - `complete`: the document and all of its sub-resources have finished
 *   loading. This is equivalent to the corresponding `readyState` value.
 */
type LoadState =
  | 'loading'
  | 'dom-interactive'
  | 'dom-content-loaded'
  | 'complete';

FirstInputPolyfillEntry

If using the "base+polyfill" build (and if the browser doesn't natively support the Event Timing API), the metric.entries reported by onFID() will contain an object that polyfills the PerformanceEventTiming entry:

type FirstInputPolyfillEntry = Omit<
  PerformanceEventTiming,
  'processingEnd' | 'toJSON'
>;

FirstInputPolyfillCallback

interface FirstInputPolyfillCallback {
  (entry: FirstInputPolyfillEntry): void;
}

NavigationTimingPolyfillEntry

If using the "base+polyfill" build (and if the browser doesn't support the Navigation Timing API Level 2 interface), the metric.entries reported by onTTFB() will contain an object that polyfills the PerformanceNavigationTiming entry using timings from the legacy performance.timing interface:

type NavigationTimingPolyfillEntry = Omit<
  PerformanceNavigationTiming,
  | 'initiatorType'
  | 'nextHopProtocol'
  | 'redirectCount'
  | 'transferSize'
  | 'encodedBodySize'
  | 'decodedBodySize'
  | 'type'
> & {
  type: PerformanceNavigationTiming['type'];
};

WebVitalsGlobal

If using the "base+polyfill" build, the polyfill.js script creates the global webVitals namespace matching the following interface:

interface WebVitalsGlobal {
  firstInputPolyfill: (onFirstInput: FirstInputPolyfillCallback) => void;
  resetFirstInputPolyfill: () => void;
  firstHiddenTime: number;
}

Functions:

onCLS()

type onCLS = (callback: CLSReportCallback, opts?: ReportOpts) => void;

Calculates the CLS value for the current page and calls the callback function once the value is ready to be reported, along with all layout-shift performance entries that were used in the metric value calculation. The reported value is a double (corresponding to a layout shift score).

If the reportAllChanges configuration option is set to true, the callback function will be called as soon as the value is initially determined as well as any time the value changes throughout the page lifespan (Note not necessarily for every layout shift).

Important: CLS should be continually monitored for changes throughout the entire lifespan of a page—including if the user returns to the page after it's been hidden/backgrounded. However, since browsers often will not fire additional callbacks once the user has backgrounded a page, callback is always called when the page's visibility state changes to hidden. As a result, the callback function might be called multiple times during the same page load (see Reporting only the delta of changes for how to manage this).

onFCP()

type onFCP = (callback: FCPReportCallback, opts?: ReportOpts) => void;

Calculates the FCP value for the current page and calls the callback function once the value is ready, along with the relevant paint performance entry used to determine the value. The reported value is a DOMHighResTimeStamp.

onFID()

type onFID = (callback: FIDReportCallback, opts?: ReportOpts) => void;

Calculates the FID value for the current page and calls the callback function once the value is ready, along with the relevant first-input performance entry used to determine the value. The reported value is a DOMHighResTimeStamp.

Important: since FID is only reported after the user interacts with the page, it's possible that it will not be reported for some page loads.

onINP()

type onINP = (callback: INPReportCallback, opts?: ReportOpts) => void;

Calculates the INP value for the current page and calls the callback function once the value is ready, along with the event performance entries reported for that interaction. The reported value is a DOMHighResTimeStamp.

A custom durationThreshold configuration option can optionally be passed to control what event-timing entries are considered for INP reporting. The default threshold is 40, which means INP scores of less than 40 are reported as 0. Note that this will not affect your 75th percentile INP value unless that value is also less than 40 (well below the recommended good threshold).

If the reportAllChanges configuration option is set to true, the callback function will be called as soon as the value is initially determined as well as any time the value changes throughout the page lifespan (Note not necessarily for every interaction).

Important: INP should be continually monitored for changes throughout the entire lifespan of a page—including if the user returns to the page after it's been hidden/backgrounded. However, since browsers often will not fire additional callbacks once the user has backgrounded a page, callback is always called when the page's visibility state changes to hidden. As a result, the callback function might be called multiple times during the same page load (see Reporting only the delta of changes for how to manage this).

onLCP()

type onLCP = (callback: LCPReportCallback, opts?: ReportOpts) => void;

Calculates the LCP value for the current page and calls the callback function once the value is ready (along with the relevant largest-contentful-paint performance entry used to determine the value). The reported value is a DOMHighResTimeStamp.

If the reportAllChanges configuration option is set to true, the callback function will be called any time a new largest-contentful-paint performance entry is dispatched, or once the final value of the metric has been determined.

onTTFB()

type onTTFB = (callback: TTFBReportCallback, opts?: ReportOpts) => void;

Calculates the TTFB value for the current page and calls the callback function once the page has loaded, along with the relevant navigation performance entry used to determine the value. The reported value is a DOMHighResTimeStamp.

Note, this function waits until after the page is loaded to call callback in order to ensure all properties of the navigation entry are populated. This is useful if you want to report on other metrics exposed by the Navigation Timing API.

For example, the TTFB metric starts from the page's time origin, which means it includes time spent on DNS lookup, connection negotiation, network latency, and server processing time.

import {onTTFB} from 'web-vitals';

onTTFB((metric) => {
  // Calculate the request time by subtracting from TTFB
  // everything that happened prior to the request starting.
  const requestTime = metric.value - metric.entries[0].requestStart;
  console.log('Request time:', requestTime);
});

Note: browsers that do not support navigation entries will fall back to using performance.timing (with the timestamps converted from epoch time to DOMHighResTimeStamp). This ensures code referencing these values (like in the example above) will work the same in all browsers.

Rating Thresholds:

The thresholds of each metric's "good", "needs improvement", and "poor" ratings are available as MetricRatingThresholds.

Example:

import {CLSThresholds, FIDThresholds, LCPThresholds} from 'web-vitals';

console.log(CLSThresholds); // [ 0.1, 0.25 ]
console.log(FIDThresholds); // [ 100, 300 ]
console.log(LCPThresholds); // [ 2500, 4000 ]

Note: It's typically not necessary (or recommended) to manually calculate metric value ratings using these thresholds. Use the Metric['rating'] supplied by the ReportCallback functions instead.

Attribution:

The following objects contain potentially-helpful debugging information that can be sent along with the metric values for the current page visit in order to help identify issues happening to real-users in the field.

See the attribution build section for details on how to use this feature.

CLS attribution:

interface CLSAttribution {
  /**
   * A selector identifying the first element (in document order) that
   * shifted when the single largest layout shift contributing to the page's
   * CLS score occurred.
   */
  largestShiftTarget?: string;
  /**
   * The time when the single largest layout shift contributing to the page's
   * CLS score occurred.
   */
  largestShiftTime?: DOMHighResTimeStamp;
  /**
   * The layout shift score of the single largest layout shift contributing to
   * the page's CLS score.
   */
  largestShiftValue?: number;
  /**
   * The `LayoutShiftEntry` representing the single largest layout shift
   * contributing to the page's CLS score. (Useful when you need more than just
   * `largestShiftTarget`, `largestShiftTime`, and `largestShiftValue`).
   */
  largestShiftEntry?: LayoutShift;
  /**
   * The first element source (in document order) among the `sources` list
   * of the `largestShiftEntry` object. (Also useful when you need more than
   * just `largestShiftTarget`, `largestShiftTime`, and `largestShiftValue`).
   */
  largestShiftSource?: LayoutShiftAttribution;
  /**
   * The loading state of the document at the time when the largest layout
   * shift contribution to the page's CLS score occurred (see `LoadState`
   * for details).
   */
  loadState?: LoadState;
}

FCP attribution:

interface FCPAttribution {
  /**
   * The time from when the user initiates loading the page until when the
   * browser receives the first byte of the response (a.k.a. TTFB).
   */
  timeToFirstByte: number;
  /**
   * The delta between TTFB and the first contentful paint (FCP).
   */
  firstByteToFCP: number;
  /**
   * The loading state of the document at the time when FCP `occurred (see
   * `LoadState` for details). Ideally, documents can paint before they finish
   * loading (e.g. the `loading` or `dom-interactive` phases).
   */
  loadState: LoadState;
  /**
   * The `PerformancePaintTiming` entry corresponding to FCP.
   */
  fcpEntry?: PerformancePaintTiming;
  /**
   * The `navigation` entry of the current page, which is useful for diagnosing
   * general page load issues. This can be used to access `serverTiming` for example:
   * navigationEntry?.serverTiming
   */
  navigationEntry?: PerformanceNavigationTiming | NavigationTimingPolyfillEntry;
}

FID attribution:

interface FIDAttribution {
  /**
   * A selector identifying the element that the user interacted with. This
   * element will be the `target` of the `event` dispatched.
   */
  eventTarget: string;
  /**
   * The time when the user interacted. This time will match the `timeStamp`
   * value of the `event` dispatched.
   */
  eventTime: number;
  /**
   * The `type` of the `event` dispatched from the user interaction.
   */
  eventType: string;
  /**
   * The `PerformanceEventTiming` entry corresponding to FID (or the
   * polyfill entry in browsers that don't support Event Timing).
   */
  eventEntry: PerformanceEventTiming | FirstInputPolyfillEntry;
  /**
   * The loading state of the document at the time when the first interaction
   * occurred (see `LoadState` for details). If the first interaction occurred
   * while the document was loading and executing script (e.g. usually in the
   * `dom-interactive` phase) it can result in long input delays.
   */
  loadState: LoadState;
}

INP attribution:

interface INPAttribution {
  /**
   * A selector identifying the element that the user interacted with for
   * the event corresponding to INP. This element will be the `target` of the
   * `event` dispatched.
   */
  eventTarget?: string;
  /**
   * The time when the user interacted for the event corresponding to INP.
   * This time will match the `timeStamp` value of the `event` dispatched.
   */
  eventTime?: number;
  /**
   * The `type` of the `event` dispatched corresponding to INP.
   */
  eventType?: string;
  /**
   * The `PerformanceEventTiming` entry corresponding to INP.
   */
  eventEntry?: PerformanceEventTiming;
  /**
   * The loading state of the document at the time when the even corresponding
   * to INP occurred (see `LoadState` for details). If the interaction occurred
   * while the document was loading and executing script (e.g. usually in the
   * `dom-interactive` phase) it can result in long delays.
   */
  loadState?: LoadState;
}

LCP attribution:

interface LCPAttribution {
  /**
   * The element corresponding to the largest contentful paint for the page.
   */
  element?: string;
  /**
   * The URL (if applicable) of the LCP image resource. If the LCP element
   * is a text node, this value will not be set.
   */
  url?: string;
  /**
   * The time from when the user initiates loading the page until when the
   * browser receives the first byte of the response (a.k.a. TTFB). See
   * [Optimize LCP](https://web.dev/articles/optimize-lcp) for details.
   */
  timeToFirstByte: number;
  /**
   * The delta between TTFB and when the browser starts loading the LCP
   * resource (if there is one, otherwise 0). See [Optimize
   * LCP](https://web.dev/articles/optimize-lcp) for details.
   */
  resourceLoadDelay: number;
  /**
   * The total time it takes to load the LCP resource itself (if there is one,
   * otherwise 0). See [Optimize LCP](https://web.dev/articles/optimize-lcp) for
   * details.
   */
  resourceLoadTime: number;
  /**
   * The delta between when the LCP resource finishes loading until the LCP
   * element is fully rendered. See [Optimize
   * LCP](https://web.dev/articles/optimize-lcp) for details.
   */
  elementRenderDelay: number;
  /**
   * The `navigation` entry of the current page, which is useful for diagnosing
   * general page load issues. This can be used to access `serverTiming` for example:
   * navigationEntry?.serverTiming
   */
  navigationEntry?: PerformanceNavigationTiming | NavigationTimingPolyfillEntry;
  /**
   * The `resource` entry for the LCP resource (if applicable), which is useful
   * for diagnosing resource load issues.
   */
  lcpResourceEntry?: PerformanceResourceTiming;
  /**
   * The `LargestContentfulPaint` entry corresponding to LCP.
   */
  lcpEntry?: LargestContentfulPaint;
}

TTFB attribution:

interface TTFBAttribution {
  /**
   * The total time from when the user initiates loading the page to when the
   * DNS lookup begins. This includes redirects, service worker startup, and
   * HTTP cache lookup times.
   */
  waitingTime: number;
  /**
   * The total time to resolve the DNS for the current request.
   */
  dnsTime: number;
  /**
   * The total time to create the connection to the requested domain.
   */
  connectionTime: number;
  /**
   * The time time from when the request was sent until the first byte of the
   * response was received. This includes network time as well as server
   * processing time.
   */
  requestTime: number;
  /**
   * The `navigation` entry of the current page, which is useful for diagnosing
   * general page load issues. This can be used to access `serverTiming` for example:
   * navigationEntry?.serverTiming
   */
  navigationEntry?: PerformanceNavigationTiming | NavigationTimingPolyfillEntry;
}

Browser Support

The web-vitals code has been tested and will run without error in all major browsers as well as Internet Explorer back to version 9. However, some of the APIs required to capture these metrics are currently only available in Chromium-based browsers (e.g. Chrome, Edge, Opera, Samsung Internet).

Browser support for each function is as follows:

  • onCLS(): Chromium
  • onFCP(): Chromium, Firefox, Safari 14.1+
  • onFID(): Chromium, Firefox (with polyfill: Safari, Internet Explorer)
  • onINP(): Chromium
  • onLCP(): Chromium
  • onTTFB(): Chromium, Firefox, Safari 15+ (with polyfill: Safari 8+, Internet Explorer)

Limitations

The web-vitals library is primarily a wrapper around the Web APIs that measure the Web Vitals metrics, which means the limitations of those APIs will mostly apply to this library as well. More details on these limitations is available in this blog post.

The primary limitation of these APIs is they have no visibility into <iframe> content (not even same-origin iframes), which means pages that make use of iframes will likely see a difference between the data measured by this library and the data available in the Chrome User Experience Report (which does include iframe content).

For same-origin iframes, it's possible to use the web-vitals library to measure metrics, but it's tricky because it requires the developer to add the library to every frame and postMessage() the results to the parent frame for aggregation.

Note: given the lack of iframe support, the onCLS() function technically measures DCLS (Document Cumulative Layout Shift) rather than CLS, if the page includes iframes).

Development

Building the code

The web-vitals source code is written in TypeScript. To transpile the code and build the production bundles, run the following command.

npm run build

To build the code and watch for changes, run:

npm run watch

Running the tests

The web-vitals code is tested in real browsers using webdriver.io. Use the following command to run the tests:

npm test

To test any of the APIs manually, you can start the test server

npm run test:server

Then navigate to http://localhost:9090/test/<view>, where <view> is the basename of one the templates under /test/views/.

You'll likely want to combine this with npm run watch to ensure any changes you make are transpiled and rebuilt.

Integrations

License

Apache 2.0

web-vitals's People

Contributors

andersdjohnson avatar ben-larson avatar brendankenny avatar dependabot[bot] avatar jayhori avatar jodydonetti avatar jschulte avatar justin-john avatar malchata avatar manantank avatar mmocny avatar monis0395 avatar nicholasray avatar omrilotan avatar philipwalton avatar robatron avatar roderickhsiao avatar rviscomi avatar simenhansen avatar skychx avatar stephenyu avatar theneekz avatar tomayac avatar tunetheweb avatar vanderhoop avatar yuangwei avatar zizzamia avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

web-vitals's Issues

Visually Complete

Many developers and SREs use custom versions of "visually complete", but every implementation has problems. For example, client side javascript is limited and often inaccurate, extension-based screenshots are inefficient, etc.

It is a challenging problem to solve since sites vary with dynamic content such as carousels, moving ads, and user interaction, but it would help a lot to have some measurement indicating most above the fold rendering has finished. Any dynamic content after that could be ignored and any user interaction could null the measurement.

Please consider adding this as a standard measurement to web vitals. Ideally a version of this can become a standard available in the performance api.

Is CLS actually DCLS?

Judging by the implementation, it looks like the "CLS" that web-vitals reports is actually DCLS. Is that right?

GA spike of users with not set landing page after install the script

Hi !
thanks for sharing this useful piece of code :)

I've tried to install the web vitals monitoring code using GTM. Data seems to be collected correctly, but then I noted a spike of user with location (not set) in GA data (see the screenshot below). After stopping the web vitals tag the data came back to normal. It's seems like a bot it's activated via this code, as you can see in the second screenshot the bad traffic is coming from USA with Linux OS and null session duration. Can you help me with this issue please? Do you know another cdn for getting the code?

spike data
2020-07-24_16h32_59

details
2020-07-24_16h33_46

Many thanks,
Daniela

Implementation problems

Hello - We are implementing Web Vitals via GTM following the documentation here, but it does not seem to be working as expected. The tag fires on gtm.js and loads the library, but the actual metrics don't seem to be set on DOM ready. When we call them, some are defined, some are undefined. Does this in fact need to be hard-coded for the timing to work correctly? Are we missing something? Thanks in advance! ~Natty C.

LCP on user interaction?

Why is LCP reported on user interaction?

I can load my page and sit on it for minutes before clicking, then LCP is reported for those minutes instead of when the actual content is shown.

LCP is not friendly with 'carousel' hero image

This is a follow up to #60

On our website (https://www.pogo.com) we have an auto-scrolling hero image, also referred to as a 'carousel' spotlight image. The behavior of this seems to cause a new entry into LCP each time it is changed out before any user interaction. This can lead to a confusing or erroneous LCP response if the page is not interacted with by the user before the first swap occurs.
Screen Shot 2020-07-07 at 12 54 31 PM

Is there any guidance into using LCP with such behavior, or an alternative implementation?

Type declaration not found

Importing this package in a typescript project results in the error:

Could not find a declaration file for module 'web-vitals'. '/node_modules/web-vitals/dist/web-vitals.min.js' implicitly has an 'any' type.
Try npm install @types/web-vitals if it exists or add a new declaration (.d.ts) file containing declare module 'web-vitals';ts(7016)

I see the lib is written in typescript so I'm assuming this is a misconfiguration of the build, not that types are hosted in an types package

Measure relative to fetchStart?

My application is behind authentication which is handled via redirects to another domain outside my control. I've implemented logging of navigation and paint timing metrics recently and noticed the redirect time introduces significant variance in the data (depending on if a redirect for auth was needed or not).

Looking at the Navigation timing model, it also seems inappropriate to include unload time from the previous site because that is out of my app's control.

Based on this I adjusted our perf measurements to be relative to the fetchStart instead of startTime.

I wanted to get your team's input - does this seem like a reasonable thing to do for performance metrics? or am I missing some value here?

Maybe this could be added as an option to this lib.

Data Visualization - Can you provide examples/instructions?

Hello,

I started collecting Web Vitals in my Google Analytics account (using this library).
But, I have no idea how to visualize the data in order to achive a similar report as in 'Google Search Console', 'Chrome UX Dashboard' or anything basic.

It would be great if you could provide some examples/instructions.

It would be really great if you could create a DataStudio template to visualize the web vitals events in Google Analytics.
I guess this is the most common use-case and will make the many users' life easier.

Thanks!

Google Ads report into Analytics impacted by Web Vitals

Hi all, we added web-vitals project in our web side to keep track of new measures. We are used to use Google Analytics as well to follow lot of metrics and during our checks we discovered a strange and huge increments on Users (and a lot less on Sessions) in Google Ads reports on GA. We found that the increment is due to a series of hits without a campaignId. The increments are started when we published web vitals tag through Tag Manager.
Some considerations we did (hope to be correct on that):

  • GA Google Ads report is based on Source (google) and medium (cpc)
  • the (huge) difference between users and sessions in report is due to the fact web vitals are NON interactive events, instead the session exposed in the report are the interactive one.
  • source and medium are correct, they are set in the session, simply, it seems to me that every web vitals hits impact the Google Ads report as well.

Any idea on how to prevent this behaviour?

getFCP not firing

Hey

I am currently playing round with this and wrote the following code

<script
  defer
  src="https://unpkg.com/[email protected]/dist/web-vitals.es5.umd.min.js"
></script>
<script>
  const $log = document.querySelector('#log');
  const log = (newLog) => {
    $log.innerHTML += `<pre>${JSON.stringify(newLog, null, 4)}</pre>`;
  };

  addEventListener('DOMContentLoaded', function () {
    webVitals.getFCP(log);
    webVitals.getTTFB(log);
  });
</script>

However while I get a result for getTTFP I get nothing for getFCP

Is this because the page is to simple?

Following the instructions on how to send results to an analytics endpoint gives an error

In the README.md the example on how to send the results to an analytics endpoint gives TypeError: Converting circular structure to JSON. The problem is in the PerformanceEntries which cannot be converted to JSON, since they include DOM Elements. I found in the typescript definition that the PerformanceEntries have a toJSON() function (which actually has any type definition), but it doesn't help.

Please provide usable examples on how to correctly handle and send all the data to an analytics endpoint or remove the broken example and provide detailed information about what data should and could be sent to an analytics endpoint.

The problematic example I'm referring to in the README.md:

import {getCLS, getFID, getLCP} from 'web-vitals';

function sendToAnalytics(metric) {
  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});
}

getCLS(sendToAnalytics);
getFID(sendToAnalytics);
getLCP(sendToAnalytics);

Best way to detect browser support

👋 I saw #36 and had my own guestion about browser support. How do you currently recommend checking if this library is supported by the user agent?

From my reading, the recommended way to approach browser support differences is to detect the existence of an API as opposed to a particular user agent, according to this article?

Here are two snippets we're debating using, each with their own trade-offs:

Approach 1: Detect Chromium browsers

Challenges: This is error prone and will need to be updated as support changes.

function trackWebVitals() {
  const isChromiumBrowser = !!navigator.userAgent.match(/Chrome|Chromium/)
  if (!isChromiumBrowser) return
  
  ...
}

Approach 2: Check if some API exists

Is there one particular API that we could check for? Internally we were considering looking at PerformanceObserver for that.

function trackWebVitals() {
  const hasPerformanceObserver = 'PerformanceObserver' in window
  if (!hasPerformanceObserver) return

  ...
}

But the question here is – would that be enough? Browser support seems to be different for each function:

  • getCLS(): Chromium
  • getFCP(): Chromium
  • getFID(): Chromium, Firefox, Safari, Internet Explorer (with polyfill, see below)
  • getLCP(): Chromium
  • getTTFB(): Chromium, Firefox, Safari, Internet Explorer

Anyway wondering what your thoughts were. I think that ultimately it would be best if the library just abstracted away the browser support question 😄

function trackWebVitals() {
  const hasBrowserSupport = webVitals.hasFullBrowserSupport()
  if (!hasBrowserSupport) return
}

Thank you! We're excited about this 😄

CLS is not reported on first hidden if the value is still zero

Pages with a CLS value of 0 still need to report that value when the page is first hidden. However, the current callback logic won't fire a callback if the delta is 0 and the metric is not final.

This logic should be updated to also check if no previous value exists (to allow for first-time reporting of 0-delta metrics).

How should be implemented CLS ideal carousel ?

Hello,

I want to ask you how to implement ideal carousel and achieve good CLS score.

Example problem:
Carousel example using glide.js: http://jsfiddle.net/4tsf5gum/

As you can see initial HTML markup contain all visible slides. This is necessary to have a good LCP if carousel is first element on page with images. In initialization phase, library create clone slides before first and last slide and change translate3d property as explained in image.

carousel

Is it somehow possible to insert clone slides and at the same time change transform property and do not trigger layout shift ?

Thank you.

Questions about manually takeRecords

Hello, maintainers. I have a question about this line.

when I start test server and navigate to http://localhost:9090/test/lcp, and then add some log to print records return by po.takeRecords(), I find it's length always be 0.

So, I don't know in which condition, we need to manually get records and its length will not be 0.

Please help me to understand this line, thanks.

Browser Support

My team is unsure about tracking the metrics provided by this library. Rationale being that if we use these metrics then we'll be optimizing for Chromium-only browsers.

What is your team's take on the value of tracking these metrics today given the level of browser support?

Is there support or commitment from other browser vendors to implement the APIs used to gather these metrics? Might be worth mentioning this in the docs.

CLS is not reporting often

First: I'd like to thank you all for the time you put into this library!

I have a small issue with how often CLS is reporting:

On a highly trafficked site that I'm working on, we found that CLS is captured very infrequently in our analytics. In fact, only ~31% of pageviews on Chrome report a CLS metric; whereas FID is captured on ~43% (not great either) and LCP is captured on ~80% of page views.

It sounds like the implementation waits for visibility to be hidden before reporting. Is it possible that a lot of page loads are not triggering the hidden event before navigation to a new page? Any other ideas?

Capture all metrics contributing to the Lighthouse performance score

Web vitals and especially this library is a great step towards consistent metrics across a variety of tools. Thank you for this!

I am wondering whether this library could be extended to capture all the metrics contributing to the Lighthouse performance score? This would help establish a consistent experience/measurements across the whole tooling landscape. Consistent experiences/measurements could improve adoption and this in turn could improve the experience on the web. WDYT?

I realize that these metrics may not fit under the web vitals umbrella, but they do fall into a similar category.

btw.: Thank you for building this library in such a way that it is closure compiler advanced mode compatible :)

Functions as promises

Would it be possible to also support the async/await syntax if no callback is provided?

const data = await getCLS();

Watching for 'scroll' event may end LCP before a valid browser entry is created

TLDR; I believe web-vitals marks isFinal on all types of scroll events when only some scroll events stop largest-contentful-paint browser entries.

Background

Thanks for open sourcing this!

When we started trying to use getLCP in our code at page start, the reporter would never fire. We noticed that our component library's positioning changes caused a scroll event before any meaningful render, that the web-vitals' input handler would catch. The page hadn't rendered anything, so there weren't any LCP entries for web-vitals to report to our callback. All well and fine, we were causing a scroll event, and the docs describe marking isFinal on scroll.

However, in our case, the Web Chrome Vitals Extension later displayed an LCP value. The extension would call getLCP on tab-complete, well after the scroll that stopped our web-vitals instance and read LCP entries just fine.

Let me know if you want a repro. The code is still internal, so it may take some time to get a fiddle up.

I get the impression this library watches for scroll because it reads entries created by the browser, and the browser stops recording on scroll. Through my use case, though, I think I've found that this library stops on any scroll event when the browser only stops creating the largest-contentful-paint entries on some scroll events.

LCP logic in Chromium

Searching for LCP and scroll logic in Chromium, I think I might have found the corresponding files.

Many types of scrolls I think all create `Event.type = 'scroll':
https://github.com/chromium/chromium/blob/a4f27fdc4913d800752bd1cb9533d31357163d65/third_party/blink/renderer/core/scroll/scroll_types.h#L58

But LCP only stops recording on two types of scroll events:
https://github.com/chromium/chromium/blob/55f44515cd0b9e7739b434d1c62f4b7e321cd530/third_party/blink/renderer/core/paint/paint_timing_detector.cc#L188

Remediation?

I admittedly don't understand what kCompositorScroll is, but the library could listen to key up, key down, and wheel device events instead of scroll as a proxy for 'kUserScroll'.

If this is a valid bug and has a straightforward fix, let me know, I'm happy to open contribute.

Allow installation as simple <script> tag

I want to install this by just doing something like <script src="web-vitals.js"></script> and then call the functions from the documentation to track the metrics, but it seems that's not a supported way of using this? I'm not really that familiar with JavaScript modules. It would be nice if there was a simple, "old school" way to install this.

Utility for checking a metric passes the threshold?

When I first started using the library, I was a little surprised to see it didn't implement a canonical helper for determining if a metric passed the Core Web Vitals thresholds.

While our documentation on web.dev captures what the thresholds are, I can imagine there is value to a developer not needing to hardcode threshold values themselves (e.g imagine if they got this subtly wrong).

I currently implement custom scoring logic in the WIP web vitals extension, but thought I'd throw this idea over the fence to see what you thought.

I'll defer on the API shape if you think this would be helpful, but you could loosely imagine...

getCLS((result) => logCurrentValue('cls', result.value, result.passesThreshold, result.isFinal));

Unload event listeners should not be used

Given the new no-unload-listeners audit that's been added to Lighthouse, we should remove any use of the unload event. Even though the use in this library is to fix a bug and doesn't apply to the issues described in the audit, we still don't want sites to fail the audit because of this library.

Add isSupported metric property

I'm currently trying to wrap the web vitals lib into a custom <web-vitals /> element.

It would be great if there would be a way to get information about the support of a particular metric. E.g. in Firefox the reporters are just not called. I could imagine to include an isSupported property or similar in the metric object that is passed to the particular reporter. Could this be a valuable addition?

Thanks a bunch for this project, it's great!

Usage on Angular routed applications?

Hello,
First of all, great work!

I am wondering: Is it possible to use this tool on Angular routed applications?

I can definitely see the usage on the very first page of my application, since it's like any other web application. My question is more related to subsequent pages in my application flow.

Can this be able to understand that there is a route change and, therefore, restart the metric?
I haven't checked the code, but if there is nothing browser API related, but couldn't the application itself tells the tool that there is a route change? In case, of course, you can't directly detect it.

Cheers!

Question - differences between web-vitals library, web-vitals extension, and CrUX report

Hello again,

I'd like to explain that we are trying to use web-vitals library in a way so that we can get daily reports of metrics in our GA, in a similar way we see Monthly reports in our CrUX Dashboard v2.
Screen Shot 2020-07-14 at 12 27 24 PM

We're confused about how CrUX/Page Speed Insights and the web-vitals extension (https://github.com/GoogleChrome/web-vitals-extension) report metrics, because it doesn't seem to be the same way as the web-vitals library. Are there differences in how these tools report metrics vs the library?

FCP is larger than LCP

Thanks for this awesome library.

I am using GA for collecting the web vitals data. From GA dashboard, I can see FCP value is larger than LCP at some date, which is beyond our expectation(FCP should be less than LCP). Seems there is something wrong.

image

I am using web-vitals v0.2.2.

The following is my data reporting code (pls let me know if I am using it incorrectly):

import {getFCP, getLCP} from 'web-vitals';

function sendToGoogleAnalytics({name, delta, id}) {
  // Assumes the global `gtag()` function exists, see:
  // https://developers.google.com/analytics/devguides/collection/gtagjs
  gtag('event', name, {
    event_category: 'Web Vitals',
    // Google Analytics metrics must be integers, so the value is rounded.
    // For CLS the value is first multiplied by 1000 for greater precision
    // (note: increase the multiplier for greater precision if needed).
    value: Math.round(name === 'CLS' ? delta * 1000 : delta),
    // The `id` value will be unique to the current page load. When sending
    // multiple values from the same page (e.g. for CLS), Google Analytics can
    // compute a total by grouping on this ID (note: requires `eventLabel` to
    // be a dimension in your report).
    event_label: id,
    // Use a non-interaction event to avoid affecting bounce rate.
    non_interaction: true,
  });
}

getFCP(sendToGoogleAnalytics);
getLCP(sendToGoogleAnalytics);

Firefox old versions - throw an empty/unknown list of entryTypes

There is this old issue related to using PerformanceObserver with entryTypes 'paint' in Firefox v58: https://bugzilla.mozilla.org/show_bug.cgi?id=1403027 .

It is not a problem anymore, I think was fixed with v63. In case the library is looking to support older versions of Firefox, I found it could be convenient to wrap the PerformanceObserver in a try/catch for getFCP.

I can open a PR for it, but I thought to double-check first if you were interested in such a fix.

Cannot find CLS in Performance Tab

When I load the devtools on Chrome 86 when navigating to CLS demo on JSBin I can see the Layout Shift events under the Experience section but when I navigate to my site to find out what is causing it for me, I have no "Experience" section but the CLS icon constantly changes/flickers between red and green (Addy Osmani's web vitals extension).

Sorry if this is the wrong place for the issue.

CLS is reported in browsers that don't support it

Currently, the logic for when to report CLS happens outside of the check for a successfully created PerformanceObserver that can observe layout-shift entries. And since 0 values are acceptable to report for CLS, the result is other browsers will report CLS (always with the initial value of 0) even though they do not track layout shifts.

Correct location to call webVitals in a React SPA

I have a React based SPA where I am wanting to use the web-vitals library. I have it working by calling the webVitals methods within a useEffect().

The reason for the useEffect is an easy way to ensure the vitals are only recored when on a a client side renderer (the App is isomorphic)

Is this the correct way to call these methods? The component this is in is called in the main layout container that is used to wrap each page.

e.g.

import { useEffect } from "react";
import { getCLS, getFID, getLCP, getTTFB } from "web-vitals";

const WebVitals = () => {
  useEffect(() => {
    getTTFB(console.log);
    getCLS(console.log);
    getFID(console.log);
    getLCP(console.log);
  }, []);

  return null;
};

export default WebVitals;

usage

import WebVitals from './webVitals'

<WebVitals />

Why use Google Analytics Event tracking for reporting web vitals?

https://github.com/GoogleChrome/web-vitals#send-the-results-to-google-analytics

IMHO User Timing Tracking should be used. Google Analytics has been using User Time Tracking for TTFB, DIT (DOM Interactive), DNS (DNS Resolve Time) time. It supports non-integer value as well.

Example code:

import {getCLS, getFID, getLCP} from 'web-vitals';

function sendToGoogleAnalytics({name, delta, id}) {
  // https://developers.google.com/analytics/devguides/collection/analyticsjs
  ga('send', 'event', {
  hitType: 'timing',
  timingCategory: 'Web Vitals',
  timingVar: name,
  timingValue: Math.round(name === 'CLS' ? delta * 1000 : delta)
});

getCLS(sendToGoogleAnalytics);
getFID(sendToGoogleAnalytics);
getLCP(sendToGoogleAnalytics);

SPA impact

I'm concerned that the CLS will not trigger by default until a user has navigated a few times in a SPA... wouldn't that throw off the value significantly?

The main reason for incorporating this into our application is to track user experience on first load. So in that sense I feel like I'd prefer CLS to have a way of returning the current shift value when called (after DOM load in our case) which obviously hopefully would be a value of 0

Is TTFB being properly calculated?

Hello I'm probably wrong but according to this article from web dev TTFB can be calculated doing the next:

// Time to First Byte (TTFB)
var pageNav = performance.getEntriesByType("navigation")[0];
var ttfb = pageNav.responseStart - pageNav.requestStart;

But on the web-vitals implementation of TTFB we are doing the next:

      const navigationEntry = performance.getEntriesByType('navigation')[0] ||
          getNavigationEntryFromPerformanceTiming();

      metric.value = metric.delta =
          (navigationEntry as PerformanceNavigationTiming).responseStart;

I'm a bit confused of which should be the metric calculated.

Link to the file:

metric.value = metric.delta =

Thank you in advance

The isFinal flag is confusing

The initial intent of the isFinal flag was allow consumers of this library to know when LCP was finalized (since the Largest Contentful Paint API does not currently expose this information). However, given #75, the heuristics will become significantly less accurate if we're not tracking scrolls.

The isFinal flag was also a bit weird because it only really applied to LCP. CLS is not final until the page has unloaded, and FCP, FID, and TTFB are always final after the first entry is dispatched.

I've also seen examples in the wild where developers were misunderstanding isFinal and waiting until it was true before sending the data to their analytics provider. That is definitely not what isFinal is for, and something needs to be done to avoid that confusion.

CLS & Lazy Loading Images with CSS Animations

Thanks for this Chrome extension.

I noticed that CLS correlates when lazy loading images with CSS fade-in animation.

Will this going to have negative effect? Should we not use this kind of animations for lazy loading images?

Here is the code that I use for WP Rocket Lazy Load:

`/* WP-ROCKET Lazy Load */
img[data-lazy-src] {
opacity: 0;
}

/* WP-ROCKET - Upon Lazy Load */
img.lazyloaded {
-webkit-transition: opacity .25s ease-out 0.2s;
-moz-transition: opacity .25s ease-out 0.2s;
transition: opacity .25s ease-out 0.2s;
opacity: 1;
}`

Next.js: SyntaxError: Unexpected token 'export'

Following the installation in readme with Next.js v9.3.6 (latest) has led to the following issue. SyntaxError: Unexpected token 'export' Looks like a cjs module not working in React. 🤔

ERROR (CLICK TO EXPAND!)

SyntaxError: Unexpected token 'export'
(anonymous function)
/Users/ahmadawais/…/node_modules/web-vitals/dist/web-vitals.min.js:1
Module._compile
internal/modules/cjs/loader.js:892:18
Module._extensions..js
internal/modules/cjs/loader.js:973:10
Module.load
internal/modules/cjs/loader.js:812:32
Function.Module._load
internal/modules/cjs/loader.js:724:14
Module.require
internal/modules/cjs/loader.js:849:19
require
internal/modules/cjs/helpers.js:74:18
web-vitals
webpack:/external "web-vitals":1

1 | module.exports = require("web-vitals");
View compiled
webpack_require
./webpack/bootstrap:21
18 | // Execute the module function
19 | var threw = true;
20 | try {
21 | modules[moduleId].call(module.exports, module, module.exports, webpack_require);
| ^ 22 | threw = false;
23 | } finally {
24 | if(threw) delete installedModules[moduleId];
View compiled
Module../pages/index.js
/_next/development/server/static/development/pages/index.js:4450:69
webpack_require
./webpack/bootstrap:21
18 | // Execute the module function
19 | var threw = true;
20 | try {
21 | modules[moduleId].call(module.exports, module, module.exports, webpack_require);
| ^ 22 | threw = false;
23 | } finally {
24 | if(threw) delete installedModules[moduleId];
View compiled
3
/_next/development/server/static/development/pages/index.js:4996:18
webpack_require
./webpack/bootstrap:21
18 | // Execute the module function
19 | var threw = true;
20 | try {
21 | modules[moduleId].call(module.exports, module, module.exports, webpack_require);
| ^ 22 | threw = false;
23 | } finally {
24 | if(threw) delete installedModules[moduleId];
View compiled
▶ 10 stack frames were collapsed.
This screen is visible only in development. It will not appear if the app crashes in production.
Open your browser’s developer console to further inspect this error.

CLS - strange behavior?

Hello,

We noticed that getCLS(onReport) is only firing when the user changes to another tab.

We tried to use getCLS(onReport, true) and this also led us to find some strange behavior -

An animation triggered by a hover event is reporting as hadRecentInput: false, these seem to be related to different children elements having "transform: " properties.

We also noticed that a child element of hover animated element has backface-visilbity: unset, our hero carousel image (a sibling element of a higher parent container element) then triggers CLS entries (with reportAllChanges = true), however with backface-visiblity: hidden the hero carousel no longer triggers CLS, but hovering in and out from the animated element does trigger CLS again.

I have recorded a video of the steps above, please let me know if you would like to see it.

Any plans to capture TTI?

@philipwalton I notice that slowly the meaningful web metrics to collect have moved away from TTI to LCP these days. Can you shed some light on why that is the case after promoting TTI for the last couple of years by the webdev community?

I also noticed a TTI measuring lib and have been using it for a few weeks now. Would you suggest me to stop using it and if not then any plans of integrating that into web-vitals?

~Nish

Document metric units

It would be great if the README could document what units (seconds, ms, shifts etc) each metric is being reported back with. When I began using the library, I assumed it was just passing raw values back (where possible) from the WebPerf APIs but it would help to make this explicit. Perhaps include some examples of the values you'll likely get reported back and units?

Do I need to wait for DOMContentLoaded?

Hi there,

I'm unsure from your docs about whether or not it's necessary to wait for DOMContentLoaded, or some other pageload event?

In most of your examples you don't seem to wait:

import {getCLS, getFID, getLCP} from 'web-vitals';

function sendToAnalytics(metric) {
  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});
}

getCLS(sendToAnalytics);
getFID(sendToAnalytics);
getLCP(sendToAnalytics);

But then further down you do:

<!-- Load `web-vitals` using a classic script that sets the global `webVitals` object. -->
<script defer src="https://unpkg.com/[email protected]/dist/web-vitals.es5.umd.min.js"></script>
<script>
addEventListener('DOMContentLoaded', function() {
  webVitals.getCLS(console.log);
  webVitals.getFID(console.log);
  webVitals.getLCP(console.log);
});
</script>

I couldn't figure this out on my own, since I feel like there's a chance we wouldn't want to wait for the content load, because that would affect the timing?

Also, would be great to have the library do this automatically 😊

webVitals.ready(function() {
  webVitals.getCLS(console.log);
  webVitals.getFID(console.log);
  webVitals.getLCP(console.log);
})

Event not being fired or fired with no value

Hey all! I know you have this note:

Note: 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 switching tabs and then switching back.

But I realized that a few times the events are not being fired at all or with no value attached. This is the implementation based on the console.log example that you have it.

import { getCLS, getFID, getLCP } from "web-vitals";

const sendToNR = (event, value) => {
  if (window.newrelic) {
    window.newrelic.setCustomAttribute(event, value);
  }
};

const startSiteSpeed = () => {
  getCLS(metric => sendToNR("CLS", metric.value));
  getFID(metric => sendToNR("FID", metric.value));
  getLCP(metric => sendToNR("LCP", metric.value));
};

Is there anything wrong here? Also, is there an accurate way to test these metrics? The Google Chrome extension shows really different numbers for the same page.

Feature Request: Add `removeEventListener`/`unobserve` equivalent

Currently every invocation creates a new PerformanceObserver that never unobserves so there's no way to remove a handler. It'd be nice to be able to remove old listeners.

EDIT: This affects the web vitals extension which leaks listeners on every activation of tab. I'm fixing over there, but this seems generally useful too 😃

Option to batch reporting callbacks

I'm looking at setting up a simple standalone backend for collecting and visualizing web-vitals metrics. Being able to report all vitals in one go would in theory simplify that quite a lot. Unfortunately the custom analytics endpoint example in the readme will send a request for each tracked metric, which obviously results in a lot of requests if you track all the available metrics.

After reading though the source, it seems to me that the reporting callbacks will only trigger when there is data and otherwise do nothing. In theory this prevents a naive attempt at batching using Promise.all from working reliably.

Is there a better way of getting the all the tracked vitals in one go? Or is batching them somehow a bad idea?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.