Monitor & Set-up Core web vitals report with Web-vitals JavaScript

Core Web Vitals focuses on three aspects of User Experience - Loading performance (LCP), Interactivity (FID) and visual stability (CLS).

The web-vitals library is a tiny (~1K), 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).
These web-vitals JS help you generate Report and display in a histogram and timeline of each Web Vitals metric.

monitor Core web vitals with web-vitals js

In this step by step tutorial, we are going to learn how to set up & monitor Core Web vitals with web-vitals js on our project with the help of Elementor pro: Custom Code functionalities & Code Snippet plugin.

The web-vitals library is a tiny (~1K), 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).

I will focus mostly on improving Cummulative layout shift (CLS) problems in this article.

Core web vitals report

Why do we need to monitor Core Web Vitals?

This is not about SEO benefits but more about what your actual/real users are experiencing when they visit your website.

From my experiences, a Good Page speed Insights (PSI) or Lighthouse score is not the same as the Field-data score. Because I have tested it on my most 2 popular blog posts.

  1. Sticky header with Elementor.
  2. Remove unused CSS from Elementor

And the result comes out to be the opposite. You can see it here :

  1. Lighthouse ( Native speed)
  2. Field-data collected by web-vitals JS libraries

Lab-data is synthetically collected in a testing environment, is critical for tracking down bugs and diagnosing issues because it is reproducible and has an immediate feedback loop like lazy loading images fixes the “defer offscreen images”.

Field-data allows you to understand what real-world users are experiencing, conditions that are hard to simulate in the lab like latency, different devices, cache condition etc

If you really want to know, follow these links why Lighthouse score and Field-data mismatch?

GTmetrix lighouse Score
Site speed testing in 4G LTE connection

Field-Data collected by web-vitals.js libraries ( this is past 2 days data by the way)

Data collected by web-vitals JS is totally different from Lighthouse score

As you can see from the screenshot above (lighthouse and Field report) both are different from each other. Focus on LCP and CLS.

You can see the data, it is totally mismatched.

To start collecting data, we have to set up a Custom Dimension in Google Analytics.

To do that-

  1. Login to your Google Analytics Property.
  2. Click the Admin, bottom left corner (see the screenshot below)
  3. Google Analytics Admin area
  4. After you open the Admin area, you have to Scroll down to your Property and you will find Custom Definition and open it and choose Custom Property.
  5. Custom Dimension in Google Analytics
  6. Click the New Dimension
  7. Remember the Index Number, it is important. When we define Dimension number we will use index number whether it is 1 or 2.
  8. Index number
Click the anchor links below to jump to the page for faster and easier navigation: (Method)
  1. Elementor Pro: Custom code
  2. Code Snippets

Elementor Pro

Step: #1- Custom Code

Elementor's Custom Code

In our first step, we are going to use Elementor pro: Custom Code functionalities to implement web-vitals js without writing any line of PHP code.

To Access Custom Code, make sure you have Elementor Pro to version 3.1.0 and above.

  1. Go to the Backend of your WordPress website.
  2. Hover your mouse on the Elementor.
  3. Click the Custom Code.

Custom Code is a tool that gives you one place where you can insert styles and scripts like adding Google Analytics Tracking code, metatag etc

Step: #2- Click the Add New Custom Code 

The second step is where we add web-vitals js libraries with Elementor pro: Custom code.

When you add the code, you have to define the location where the Custom Code will appear and give priority in which order the custom code will appear.

To send data to Google Analytics:

    1. We have to implement web-vitals.js libraries.
    2.  Collected the data from real users with the help of web vitals JS libraries and then sent it as web vitals events to Google Analytics. (see the screenshot below)

Web Vitals Events sent by Web vitals JS libraries

There are 3 code examples, we can use to send results to Google Analytics –

– Universal Analytics (UA) alone:

For some reason, if you got stuck to Universal Analytics, then this is the Code for you to use on your website.

Web-vitals JS with Elementor pro - Custom Code
				
					     <script type="module">
  // Load the web-vitals library from unpkg.com (or host locally):
  import {getFCP, getLCP, getCLS, getTTFB, getFID} from 'https://unpkg.com/web-vitals?module';
  
  function getSelector(node, maxLen = 100) {
    let sel = '';
    try {
      while (node && node.nodeType !== 9) {
        const part = node.id ? '#' + node.id : node.nodeName.toLowerCase() + (
          (node.className && node.className.length) ?
          '.' + Array.from(node.classList.values()).join('.') : '');
        if (sel.length + part.length > maxLen - 1) return sel || part;
        sel = sel ? part + '>' + sel : part;
        if (node.id) break;
        node = node.parentNode;
      }
    } catch (err) {
      // Do nothing...
    }
    return sel;
  }
  
  function getLargestLayoutShiftEntry(entries) {
    return entries.reduce((a, b) => a && a.value > b.value ? a : b);
  }
  
  function getLargestLayoutShiftSource(sources) {
    return sources.reduce((a, b) => {
      return a.node && a.previousRect.width * a.previousRect.height >
          b.previousRect.width * b.previousRect.height ? a : b;
    });
  }
  
  function wasFIDBeforeDCL(fidEntry) {
    const navEntry = performance.getEntriesByType('navigation')[0];
    return navEntry && fidEntry.startTime < navEntry.domContentLoadedEventStart;
  }
  
  function sendWebVitals() {
    function sendWebVitalsGAEvents({name, delta, id, entries}) {
      if ("function" == typeof ga) {
  
        let webVitalInfo = '(not set)';
  
        // Set a custom dimension for more info for any CVW breaches
        // In some cases there won't be any entries (e.g. if CLS is 0,
        // or for LCP after a bfcache restore), so we have to check first.
        if (entries.length) {
          if (name === 'LCP') {
             const lastEntry = entries[entries.length - 1];
             webVitalInfo =  getSelector(lastEntry.element);
          } else if (name === 'FID') {
            const firstEntry = entries[0];
            webVitalInfo = getSelector(firstEntry.target);
          } else if (name === 'CLS') {
             const largestEntry = getLargestLayoutShiftEntry(entries);
             if (largestEntry && largestEntry.sources) {
                const largestSource = getLargestLayoutShiftSource(largestEntry.sources);
                if (largestSource) {
                  webVitalInfo = getSelector(largestSource.node);
                }
             }
          }
        }
  
        ga('send', 'event', {
          eventCategory: 'Web Vitals',
          eventAction: name,
          // 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).
          eventLabel: id,
          // 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).
          eventValue: Math.round(name === 'CLS' ? delta * 1000 : delta),
          // Use a non-interaction event to avoid affecting bounce rate.
          nonInteraction: true,
          // Use `sendBeacon()` if the browser supports it.
          transport: 'beacon',
  
          // OPTIONAL: any additional params or debug info here.
          // See: https://web.dev/debug-web-vitals-in-the-field/
          // dimension1: '...',
          // dimension2: '...',
          dimension2: webVitalInfo
          // ...
        });
      }
    }

    // Register function to send Core Web Vitals and other metrics as they become available
    getFCP(sendWebVitalsGAEvents);
    getLCP(sendWebVitalsGAEvents);
    getCLS(sendWebVitalsGAEvents);
    getTTFB(sendWebVitalsGAEvents);
    getFID(sendWebVitalsGAEvents);
  
  }
  
  sendWebVitals();
</script>
				
			

– Using Google Tag Manager + Universal Analytics

If you’re using Google Tag Manager + Universal Analytics, you can also monitor Core Web Vitals with the help of the tutorials.

 – Using Google Tag Manager + Google Analytics 4 (GA4)

If you’re using Google Tag Manager with GA4 or Google Analytics 4, then I highly recommend using the Simo post on how to Track Core Web Vitals in GA4 with Google Tag Manager.

But If you’re not using Elementor pro, there are plugins you can use to implement web-vitals.js libraries. ( These are the plugin that comes up in my mind while I was writing)

  1. Header and Footer Scripts by Digital Liberation.
  2. Head and Footer Scripts Inserter By Space X-Chimp.
  3. Code Snippets By Code Snippets Pro

#2. Code Snippet:

Step: #1- Universal Analytics

  1. if you’re using Google Tag Manager + Universal Analytics: Please follow this tutorial
  2. If you’re using Google Tag Manager + Google Analytics 4: Follow this tutorial

If you don’t have Elementor pro, You can use the Code snippets plugin from WP Repository or upload it using an FTP client to implement & monitor Core web vitals.

Download The Code snippets plugin from WP Repository

Step: #2- Click the Add new

Click the add new to add the new snippet for use it is web-vitals.js

Add new Snippet

Step: #3- Deploy the new snippet with PHP

Click the Add new button and add the php to track web-vitals.js
				
					function web_vitals(  ){
	// The Data (LCP, CLS and FID) collected by web-vitals.js are not sent to Google Analytics but display on console tab. 
	//You will only see it if you're login user
if ( is_user_logged_in() ){ 
?>
<script type="module">
  import {getCLS, getFID, getLCP} from 'https://unpkg.com/web-vitals?module';
  getCLS(console.log);
  getFID(console.log);
  getLCP(console.log);
</script>

	<?php
  }
  
	// The Data (LCP, CLS and FID) collected by web-vitals.js are sent to Universal Analytics. 

	else{
		?>
// NOTE set up a new dimension in Google Analytics and then add the dimension number on line 85

// Based on Phil Walton's post: https://web.dev/debug-web-vitals-in-the-field/

<script type="module">
  // Load the web-vitals library from unpkg.com (or host locally):
  import {getFCP, getLCP, getCLS, getTTFB, getFID} from 'https://unpkg.com/web-vitals?module';
  
  function getSelector(node, maxLen = 100) {
    let sel = '';
    try {
      while (node && node.nodeType !== 9) {
        const part = node.id ? '#' + node.id : node.nodeName.toLowerCase() + (
          (node.className && node.className.length) ?
          '.' + Array.from(node.classList.values()).join('.') : '');
        if (sel.length + part.length > maxLen - 1) return sel || part;
        sel = sel ? part + '>' + sel : part;
        if (node.id) break;
        node = node.parentNode;
      }
    } catch (err) {
      // Do nothing...
    }
    return sel;
  }
  
  function getLargestLayoutShiftEntry(entries) {
    return entries.reduce((a, b) => a && a.value > b.value ? a : b);
  }
  
  function getLargestLayoutShiftSource(sources) {
    return sources.reduce((a, b) => {
      return a.node && a.previousRect.width * a.previousRect.height >
          b.previousRect.width * b.previousRect.height ? a : b;
    });
  }
  
  function wasFIDBeforeDCL(fidEntry) {
    const navEntry = performance.getEntriesByType('navigation')[0];
    return navEntry && fidEntry.startTime < navEntry.domContentLoadedEventStart;
  }
  
  function sendWebVitals() {
    function sendWebVitalsGAEvents({name, delta, id, entries}) {
      if ("function" == typeof ga) {
  
        let webVitalInfo = '(not set)';
  
        // Set a custom dimension for more info for any CVW breaches
        // In some cases there won't be any entries (e.g. if CLS is 0,
        // or for LCP after a bfcache restore), so we have to check first.
        if (entries.length) {
          if (name === 'LCP') {
             const lastEntry = entries[entries.length - 1];
             webVitalInfo =  getSelector(lastEntry.element);
          } else if (name === 'FID') {
            const firstEntry = entries[0];
            webVitalInfo = getSelector(firstEntry.target);
          } else if (name === 'CLS') {
             const largestEntry = getLargestLayoutShiftEntry(entries);
             if (largestEntry && largestEntry.sources) {
                const largestSource = getLargestLayoutShiftSource(largestEntry.sources);
                if (largestSource) {
                  webVitalInfo = getSelector(largestSource.node);
                }
             }
          }
        }
  
        ga('send', 'event', {
          eventCategory: 'Web Vitals Fix 19th may ',
          eventAction: name,
          // 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).
          eventLabel: id,
          // 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).
          eventValue: Math.round(name === 'CLS' ? delta * 1000 : delta),
          // Use a non-interaction event to avoid affecting bounce rate.
          nonInteraction: true,
          // Use `sendBeacon()` if the browser supports it.
          transport: 'beacon',
  
          // OPTIONAL: any additional params or debug info here.
          // See: https://web.dev/debug-web-vitals-in-the-field/
          // dimension1: '...',
          // dimension2: '...',
          dimension2: webVitalInfo
          // ...
        });
      }
    }

    // Register function to send Core Web Vitals and other metrics as they become available
    getFCP(sendWebVitalsGAEvents);
    getLCP(sendWebVitalsGAEvents);
    getCLS(sendWebVitalsGAEvents);
    getTTFB(sendWebVitalsGAEvents);
    getFID(sendWebVitalsGAEvents);
  
  }
  
  sendWebVitals();
</script>
<?php
	}
}
add_action('wp_head', 'web_vitals');
				
			

Step: #4- See the Chrome developer tools Console tab

To see the results properly, first, you have to right-click on your mouse and click the inspect elements to open the Google Chrome Developer panel.

Right click on any page with your mouse and do inspect elements to open dev tools

Now, We open Chrome Developer tools by right-clicking on any page and clicking the inspect elements. 

In the next steps, we have to throttle our network connection to Fast 3G so it loads slower, so we can simulate certain network conditions.

Click the network panel and check the throttling tab to throttle the network
  1. Go to the Network panel.
  2. Throttle the connection to Fast 3G to simulate the network condition.
Click the Console tab and do the Hard reload by Ctrl+ F5 or do a long hard press on reload button.
Long press on reload page button to empty cache and reload the page, so it simulate first time visitors

And Go to the Console tab and you will Core web vitals metrics individuals.

If you are login users, you will see the individuals metrics on the Chrome Console panel.

Console Log tab in Chrome Dev tools

For visitors, they will not see the console.log, instead, it will send the data to Google analytics.

Step: #5- Web Vitals Report

Give some time to collect Data from real-users (2 days is enough if you don’t have enough traffic, data that might not show up) after you implement web-vitals JS libraries.

Now head over to Web Vitals Report

Web Vitals Report

Now you have to log in to your Google Analytics account to retrieve data.

For privacy reasons, I have removed my email address.

After you click the login button, it show pop up and ask your credentials

Choose your Google Analytics property, choose the date range and compare segments and then click the submit button.

Before the optimization:

Keeps this in mind, that I only optimize LCP and CLS

Choose The Google analytics Property
These are the results from top countries and Pages from the past 7 days (your data might differ from mine).
Data retrieve from Google Analytics for top countries and pages
Top pages in web Vitals report

Now you can see my Core Web Vitals score (above which I collected from real-users) which is bad and need immediate action.

And that what I did in few days after I collected.

After the optimization:

ga:dim

To view Debug Info, you need to define Debug dimension as “ga:dimension1” without quotation marks.

These are the results from top countries and Pages from the past 29 days.

Results breakdown by Top countries in Web vitals report
Results breakdown by Top Pages in Web vitals report
Web Vitals showing debug information

Even my Google Search Console(GSC) Cumulative Layout Shift(CLS) issue is gone. That a huge improvement in my opinion. 

And I hope you will figure it out the same as mine.

Google Search Console Enhancements section Core Web Vitals reports