Google Chrome web-vitals.js v4 to support LoAF + INP breakdown

Summary: Google Chrome released a beta version of web-vitals.js (v4 beta) with a focus on INP and LoAF. We gave it a spin.

Google Chrome web-vitals.js v4 to support LoAF + INP breakdown

It's no secret that RUMvision is built on top of the Google Chrome's official web-vitals library. Google's library aims to mimic the Core Web Vitals as much as possible, allowing site owners to track the vital user experience metrics themselves. Either by directly using web-vitals or by utilizing a SaaS provider like ours.

Main changes in web-vitals.js v4

While being in beta, Barry Pollard is stating that web-vitals.js v4 includes a couple of exciting new additions:

  • INP breakdown phases
  • Long Animation Frame API information for the INP interaction
  • Renaming of attribution related object keys
  • getXXX() functions were already deprecated before and now removed in v4
  • Deprecated the onFID() function (but it was not yet removed)

Using web-vitals.js in DevTools

You might be using Google's official web-vitals Chrome extension already. It's not really that different. As a matter of fact, the extension was a bit ahead of the JS library as it has been exposing INP breakdown information already.

Nevertheless, you might want to give the JS library a spin as well. There are different ways to do so, but here's a quick start:

(function() {
  var script = document.createElement('script');
  script.src = 'https://unpkg.com/web-vitals@4.0.0-beta.1/dist/web-vitals.attribution.iife.js';
  script.onload = function() {
    webVitals.onINP( console.log );
  }
  document.head.appendChild(script);
}());

Copy and paste this into the JavaScript console in your DevTools to observe how metric information is displayed.

Using the attribution build is a crucial step to obtain breakdown information and Long Animation Frame entries.

The console.log function is passed as callback to directly log the result of the INP metric to the console. You can pass your own callback function to perform additional tasks based on the metric's data, and then submit it to your endpoint.

Exposed metrics

In the example above, I specifically used onINP as version 4 of the web-vitals.js library v4 especially comes with INP and LoAF additions. However, you could add other metrics as well. You could do this manually for onTTFB, onFCP, onLCP and onCLS.

Alternatively, you could loop over available functions and when the function name starts with an 'on' prefix and only then hook into that function and pass your callback to it. You would then end up with the following:

(function() {
  var script = document.createElement('script');
  script.src = 'https://unpkg.com/web-vitals@4.0.0-beta.1/dist/web-vitals.attribution.iife.js';
  script.onload = function() {
    Object.keys(webVitals).forEach(function(fn) {
	  fn.indexOf('on') === 0 && webVitals[fn](console.log);
    });
  }
  document.head.appendChild(script);
}());

Fun fact: the above is what RUMvision is doing to hook into all available metrics. However, despite still being available in both web-vitals JS version 3 as well as 4 we did stop tracking FID because of its deprecated status.

Moment of exposure

Please note that you might not get to see as much metrics as you would expect. This is because not all metrics are immediately reported. It is important to note that this behavior isn't specific to v4. It used to be like this for all versions of web-vitals JS.

TTFB & LCP

TTFB and FCP are reported as soon as they occur. However, there might be a slight delay on websites with more JavaScript or console activity, as the browser needs to be idle enough to report them to the console.

LCP

LCP is reported as soon as an interaction happens, as that is when even Google's actual Core Web Vitals stops listening for new LCP candidates. This means you would first need to click within the page's viewport before seeing the LCP being reported in your DevTools console.

Read about all LCP nuances and characteristics.

FID

Although deprecated, you would see the same effect for FID.

CLS and INP

If you think the above LCP behaviour is unexpected, then you might really be surprised to not see any CLS nor INP data.
That's because both metrics are finalized by the Google Chrome browser when the user navigated away from the page. This is done to be sure that only the occurence with the highest value is being reported.

Read about all nuances of CLS and INP characteristics.

INP breakdown and LoAF entries

So, it's mainly additional INP information, including both a breakdown and LoAF entries.

In case you're unfamiliar with LoAF:

  • LoAF stands for Long Animation Frame;
  • It's a game changer in debugging long browser tasks that are blocking the main thread;
  • In short, it's like git blame or basically name and shame for third party work, but also 1st party work (like hydration).

LoAF was actually officially shipped in Chrome v123 after spending some time in origin trial phase. For a bit of perspective, Chrome v123 was in stable less than a month, meaning that the Google Chrome team is rather quick in including LoAF in their web-vitals.js library.

Differences between v3 and v4

In web-vitals v3, next to a loadState flag we would get eventEntry, eventTarget and eventTime flags when using the attribution object. In short, the event-prefix has been renamed to interaction. But in web-vitals.js v4, you will also get:

  • nextPaintTime, which I think is very convenient to know;
  • and breakdown information: inputDelay, processingTime & presentationDelay;
  • eventEntry was renamed to processedEventEntries, while (as the name is suggesting due to its plural form) processedEventEntries is now an array of (potentially multiple) instances of PerformanceEventTiming's
  • and the biggest change: longAnimationFrameEntries was added, which again is an array of one or more instances of PerformanceLongAnimationFrameTiming, so an actual LoAF.

This also is where things can get complicated as one LoAF is then likely to contain multiple script's.

Apart from the INP changes mentioned above, LCP's resourceLoadTime was renamed to resourceLoadDuration. See their v4 changelog and for all breaking changes.

Although not INP related, please note that TTFB attribution keys were renamed as well from *Time to *Duration. See their upgrading guide.

An INP + LoAF example

Let's go over an INP object:

{
    "interactionTarget": "#toggle-nav",
    "interactionType": "pointer",
    "interactionTime": 1124.8999999761581,
    "nextPaintTime": 1332.8999999761581,
    "processedEventEntries": [
        ...
        {
            "name": "click",
            "entryType": "event",
            "startTime": 1193.7999999821186,
            "duration": 136,
            "navigationId": "cfebf9f3-69e7-49aa-9a5b-5ef38e8c43c3",
            "processingStart": 1217.699999988079,
            "processingEnd": 1223.699999988079,
            "cancelable": true
        }
    ],
    "longAnimationFrameEntries": [
        {
            "name": "long-animation-frame",
            "entryType": "long-animation-frame",
            "startTime": 1012.0999999940395,
            "duration": 202,
            "navigationId": "cfebf9f3-69e7-49aa-9a5b-5ef38e8c43c3",
            "renderStart": 1072.8999999761581,
            "styleAndLayoutStart": 1072.8999999761581,
            "firstUIEventTimestamp": 0,
            "blockingDuration": 150,
            "scripts": [
                {
                    "name": "script",
                    "entryType": "script",
                    "startTime": 1049,
                    "duration": 10,
                    "navigationId": "cfebf9f3-69e7-49aa-9a5b-5ef38e8c43c3",
                    "invoker": "https://www.rumvision.com/#nav",
                    "invokerType": "classic-script",
                    "windowAttribution": "self",
                    "executionStart": 1057.699999988079,
                    "forcedStyleAndLayoutDuration": 0,
                    "pauseDuration": 0,
                    "sourceURL": "https://www.rumvision.com/#nav",
                    "sourceFunctionName": "",
                    "sourceCharPosition": 0
                },
                {
                    "name": "script",
                    "entryType": "script",
                    "startTime": 1063.8999999761581,
                    "duration": 6,
                    "navigationId": "cfebf9f3-69e7-49aa-9a5b-5ef38e8c43c3",
                    "invoker": "https://www.rumvision.com/#nav",
                    "invokerType": "classic-script",
                    "windowAttribution": "self",
                    "executionStart": 1070.0999999940395,
                    "forcedStyleAndLayoutDuration": 0,
                    "pauseDuration": 0,
                    "sourceURL": "https://www.rumvision.com/#nav",
                    "sourceFunctionName": "",
                    "sourceCharPosition": 0
                }
            ]
        },
        {
            "name": "long-animation-frame",
            "entryType": "long-animation-frame",
            "startTime": 1215.2999999821186,
            "duration": 113,
            "navigationId": "cfebf9f3-69e7-49aa-9a5b-5ef38e8c43c3",
            "renderStart": 1325.7999999821186,
            "styleAndLayoutStart": 1326.0999999940395,
            "firstUIEventTimestamp": 0,
            "blockingDuration": 53,
            "scripts": [
                {
                    "name": "script",
                    "entryType": "script",
                    "startTime": 1225.5999999940395,
                    "duration": 71,
                    "navigationId": "cfebf9f3-69e7-49aa-9a5b-5ef38e8c43c3",
                    "invoker": "https://www.rumvision.com/file/cdn/ajax/libs/jquery/3.5.1/jquery.min.js",
                    "invokerType": "classic-script",
                    "windowAttribution": "self",
                    "executionStart": 1227.699999988079,
                    "forcedStyleAndLayoutDuration": 0,
                    "pauseDuration": 0,
                    "sourceURL": "https://www.rumvision.com/file/cdn/ajax/libs/jquery/3.5.1/jquery.min.js",
                    "sourceFunctionName": "",
                    "sourceCharPosition": 0
                },
                {
                    "name": "script",
                    "entryType": "script",
                    "startTime": 1297.8999999761581,
                    "duration": 19,
                    "navigationId": "cfebf9f3-69e7-49aa-9a5b-5ef38e8c43c3",
                    "invoker": "https://www.rumvision.com/file/min/1aaf28b0aec420ebf03598305225278d.js",
                    "invokerType": "classic-script",
                    "windowAttribution": "self",
                    "executionStart": 1299.5999999940395,
                    "forcedStyleAndLayoutDuration": 0,
                    "pauseDuration": 0,
                    "sourceURL": "https://www.rumvision.com/file/min/1aaf28b0aec420ebf03598305225278d.js",
                    "sourceFunctionName": "",
                    "sourceCharPosition": 0
                }
            ]
        }
    ],
    "inputDelay": 90.80000001192093,
    "processingDuration": 8,
    "presentationDelay": 109.19999998807907,
    "loadState": "loading"
}

This is an example of my very own test in Google Chrome with CPU throttled to 4x slowdown to have a higher chance of running into longer than average tasks and getting interesting data. With a reduced viewport size to trigger the mobile navigation, I then tried to click on the hamburger menu a few times.

And we see that there are multiple reported LoAFs, and each LoAF contains multiple scripts. In the example, my interaction happened at 1124ms. But some main thread work was running from 1012ms (startTime of the first LoAF) to ( + its duration, which is 202ms = ) 1215ms.

That's already overlapping my moment of interaction at 1124ms. But right after, another LoAF starts, pushing back next paint (nextPaintTime happens at 1332ms) even more. This explains why 2 LoAFs are included in the longAnimationFrameEntries object.

What's next?

It's worth mentioning that v4 is still in beta."If you use the code example I provided above you'll notice in your network requests that the resource being embedded by the JavaScript snippet results in a redirect to https://unpkg.com/web-vitals@4.0.0-beta.0/dist/web-vitals.attribution.iife.js.

Bugs & issues

The Google team is looking forward to take it out of beta soon! Meanwhile, any bugs, issues etc can be shared over at their github page.

Changes at RUMvision

Regarding RUMvision, there won't be noticeable changes, despite experimenting and implementing v4 already. We did encounter a bug during implementation, which was promptly resolved by the Google Chrome team within a few hours. However, since web-vitals v4 is still in beta, it's not the default version that we include with our RUM script.

RUMvision was an early adopter of the LoAF API. This means we're already exposing INP and LoAF breakdown in our features and dashboards. As a result and because of a generally smooth and backwards compatible implementation nothing will change from a RUMvision user perspective.

Share blog post