Skip to Main Content

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, I am going to show you how to set up & monitor Core Web vitals with web-vitals js libraries 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 be focusing mostly on improving the Cummulative Layout shift (CLS) and the Largest Contentful Paint (LCP) problems in my article.

Core web vitals report

If you’re fixing your Core Web Vitals issues, you need to first learn what is field data and lab data.

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 want to know why your Lab & field data are different? Here is the answer from the web.dev.

This is the test I run on GTMetrix on the specific network conditions.

Testing Environments

Native speed: No throttling

Test Running in no data cap speed connection using Chrome Browser and the LCP is 700ms
WebPageTest Running in LTE speed connection using Chrome Browser and the LCP is 1sec

We can see the decrease in LCP performance from 731 ms to 1 sec just by changing network connection type in the Lab environment, these drastic changes can happen when someone visits my website from slower connections.

You can see my page level – field data below-

LCP Performance when changing Network Connection

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

If you didn’t optimize your website properly, your data might be different from mine.

You can see the data, it is mismatched. This is mainly due to many things:

  1. Inconsistent in CDN performance.
  2. My server response time (TTFB)
  3. Visitor’s network connections.
  4. Location, etc.

These types of conditions are out of my control which means I can’t cache the resources etc.

Since we can’t control them but we can start monitoring Core Web Vitals performance on ourselves, you need to have:

  1. Google Analytics such as Universal Analytics & GA4.
  2. Elementor Pro/ Code snippets plugin/ a Child theme to inject the script.

To do that-

This entire tutorial is meant for Universal Analytics users, not GA4 or GTM with GA.

1) Universal Analytics (UA) users

We have to send the data/ events through custom dimensions, so we have to set up a Custom Dimension in Google Analytics.

To do that, follow the step-by-step guide-

  1. Login to your Universal Analytics Property.
  2. Click the Admin, bottom left corner (see the screenshot below)
  3. Google Analytics Admin area
  4. Under Property Tab - scroll down and Custom Definitions then find Custom Dimensions (see screenshot for more details)
  5. Custom Dimension in Google Analytics
  6. Click the New custom Dimensions button in red and add a new dimension - give a Custom Dimensions name and click the create button
  7. picture showing how to create custom dimensions in Universal Analytics
  8. Remember the Index value properly, it is important. Your index value can be different than mine.
  9. Index number
We set up Custom Dimension in UA, now, to collect and send the data through Custom Dimensions we need to set up Web Vitals JS libraries.

Setting up Web Vitals JS libraries

To monitor and send Core Web Vitals events, we have to set up web vitals JS libraries and send the web vitals events to Google Analytics and filter it out with custom dimensions.

To set up, we have to use 3rd parties plugins like Elementor Pro: Custom Code, Code Snippet, etc to inject the script.

#1. Elementor Pro

If you’re using Elementor Pro, you can use Custom Code and set up web-vitals JS without touching even a line of PHP code.

Step#1(A): Custom Code

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

  • Go to the Backend of your WordPress website.
  • Hover your mouse on the Elementor.
  • 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

Elementor's Custom Code
Step#1(B): Click the Add New button:
The second step is where we add web-vitals js libraries.
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.
Add code snippets to your page without writing any php code with Elementor pro custom code
Step#1(C): Add the web vitals JS code

For some reason, if the code provided is not working properly. Head over to Smashing Magazine JS to get the correct syntax.

If you use Smashing Magazine syntax you will need to add additional custom dimensions for dataSaver, effectiveType (network connection) & deviceMemory.

When web vitals send the events to UA it will send debug identifiers in custom dimensions. 

Remember your Custom Dimension- Index value

				
					  <!--You can host web vitals JS locally on your server-->
  
     <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;
  }
//   After we collected web vitals data from real users, we have to send as events to Google Analytics  
  
  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: '...',
          // I use dimemsion2 because my Index value is 2, if your index value is 1 then set it yo dimension1
          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>
				
			
Step#1(D): Set up Display Condition:

After you added the code now click the Display condition and set it to the entire site. You can choose the priorities and location where your code will appear.

So when your visitors visit your website, web-vitals JS will automatically collect the data and send the web vitals events to Google Analytics.

See the screenshot below

Web Vitals Events sent by Web vitals JS libraries

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 (highly recommend)

#2. Code Snippets Plugins:

  1. Step#2(A): Install the plugin:
    If you don’t have Elementor pro, You can use the Code snippets plugin from WP Repository or upload it using an FTP client.
Download The Code snippets plugin from WP Repository
  1. Step#2(B): Click the Add new:
Add new Snippet
Step#2(C): Deploy the new snippet with PHP

Instead of inline the JS to the head or body open, I combine the JS into a script tag.

				
					

function web_vitals() {
   
?>
<!--You can host web vitals JS locally on your server-->

     <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;
  }
//   After we collected web vitals data from real users, we have to send as events to Google Analytics

  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>

<?php

}
add_action('wp_footer', 'web_vitals');

//OR
// Combine the JS into an external script tag 
//

<!--First We enqueue the script and then load the JS in JS module-->

function web_vitals_int(){
    wp_enqueue_script('web-vitals','/wp-content/assets/js/web-vitals-int.min.js', false, '1.01', 'all');
}
add_action('wp_enqueue_scripts','web_vitals_int');


/*
* We Add the Module to web-vitals js libraries
*/
function add_type_attribute_web_vitals($tag, $handle, $src) {
    
    // if not your script, do nothing and return original $tag
    if ( 'web-vitals' !== $handle ) {
        return $tag;
    }
    // change the script tag by adding type="module" and return it.
    $tag = '<script type="module" defer src="' . esc_url( $src ) . '"></script>';
    
    return $tag;
}
add_filter('script_loader_tag', 'add_type_attribute_web_vitals' , 10, 3);

				
			

When we plot rows and set the secondary dimension to web Vitals Debug Info (that is the name of Custom Dimension). We were able to see the debug identifier which CSS selector causes layout shift but we don’t know the numbers exactly whether I pass the Core Web Vitals or not.

Core Web Vitals events with Custom Events as secondary dimension

To see properly, we have to head over to web vitals report and analysis the result.

Web Vitals Report 

After you set up web vitals JS libraries properly,now give some time to collect data from real users. So you can analyze it-

  1. Head over to Web Vitals Report
Web Vitals Report
  1. Login to your Google Analytics Account– Now you have to log in to your Google Analytics account to retrieve data. For privacy reasons, I have blurred my email address.
After you click the login button, it show pop up and ask your credentials
  1. Click the check box to Use advanced options (configurable per account) and add debug information:
    a) choose your Google Analytics property.
    b) choose the date range and compare segments
    c) Add
    ga:dimension1 (mine is ga:dimension2) to debug information input fill
    d) Click the submit button. 
ga:dim

These are the results from top countries and Pages from the past 7 days (your data might differ from mine).

Results-

Before the optimization:

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.

After optimizations, I did in a few days and these are the results.

After the optimization:

ga:dim

These are the results from top countries and Pages from the past 4 months.

Web vitals Report showing which countries has best and worst Core Web vitals score
After Optimization CWV page

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

3) 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 or Follow web.dev article on measure and debug performance with GA 4 with BigQuery

4) Google Analytics 4 (GA4) 

if you’re a GA4 user, I highly recommend reading the bigcommerce website advantage article on Core Web Vitals Tracking with a GA4 article.

If you see the before & after the optimization. We can see the improvement in Core Web Vitals assessments thanks to the Web Vitals JS libraries team, Barry Pollard’s article on Smashing Magazine, and Philip Walton’s article on the web.dev.

Now thanks to them, I pass the Core Web Vitals Assessments for more than 5 months and still counting.

Google Search Console

Pass Core Web Vitals Assessment
Picture showing my average LCP score is 1.7sec
Page Experience Report showing all the page pass Core Web Vitals

BigQuery + GA4

Picture shows my website passes Core Web Vitals assesements

Do you know we also send Debug identifiers in UA or GA4?

Now, because of debug identifers, we can decide whether it is a bad idea or a good idea to preload hero image. Follow my tutorials to learn more and decide it for yourself.

Get Help!

If you need help setting up Web Vitals JS libraries with Google Analytics or optimizing your website so it passes your Core Web Vitals Assessments.

Contact me on Facebook or Elementor Experts Network.

Thangjam Kishorchand

Thangjam Kishorchand

Hi there, this is my place where I write about my Elementor tips and tricks that I learned for the last 2 years. I am mostly active on Quora and Facebook. I love messing around with design trends like Variable Fonts, Dark Mode

Powered by Elementor pro

This site is powered by Elementor pro : Theme builder and it contains Affiliate links,which means that if you buy from my links, Foxscribbler will earn a small commission.This commission comes at no additional cost to you.