Skip to Main Content

Monitor & set up Core Web Vitals Report with the web-vitals JS

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

In this tutorial, I am going to show you how to set up Web Vitals JS libraries to collect the Core Web Vitals metrics from the real users and send them back to GA4 events and export the data to BigQuery with BigQuery links and use the Data Studio to visualize it.

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 daily with the help of web-vitals js libraries.

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

Before setting up the libraries, you need to first learn what is field data and lab data.

Lab data vs Field data

Lab data

Lab data is determined by loading a web page in a controlled environment with a predefined set of network and device conditions. These conditions are known as a lab environment, sometimes also referred to as a synthetic environment.

Chrome tools that report lab data are generally running Lighthouse.

The purpose of a lab test is to control for as many factors as you can, so the results are (as much as possible) consistent and reproducible from run to run.

Field Data

Field data is determined by monitoring all users who visit a page and measuring a given set of performance metrics for each one of those users’ individual experiences. Because field data is based on real-user visits, it reflects the actual devices, network conditions, and geographic locations of your users.

Field data is also commonly known as Real User Monitoring (RUM) data; the two terms are interchangeable.

Setting up Web Vitals JS libraries

Watch the video properly on how to Measure Core Web Vitals (CWV) performance with web-vitals.js, Google Analytics 4 (GA4), and BigQuery.

Copy and paste the web vitals JS libraries code to your editor and save it as a .js extension. We will use this later

Don’t use the latest version of Web vitals JS libraries. For some reason, the LCP event was not sent to GA4. Use 2.1.3 instead.
				
					/*
* Version 2.1.3
* @modules
*/
var e,t,n,i,r = function (e, t) {return { name: e, value: void 0 === t ? -1 : t, delta: 0, entries: [], id: "v2-".concat(Date.now(), "-").concat(Math.floor(8999999999999 * Math.random()) + 1e12) };},a = function (e, t) {try {if (PerformanceObserver.supportedEntryTypes.includes(e)) {if ("first-input" === e && !("PerformanceEventTiming" in self)) return;var n = new PerformanceObserver(function (e) {return e.getEntries().map(t);});return n.observe({ type: e, buffered: !0 }), n;}} catch (e) {}},o = function (e, t) {var n = function n(i) {"pagehide" !== i.type && "hidden" !== document.visibilityState || (e(i), t && (removeEventListener("visibilitychange", n, !0), removeEventListener("pagehide", n, !0)));};addEventListener("visibilitychange", n, !0), addEventListener("pagehide", n, !0);},u = function (e) {addEventListener("pageshow", function (t) {t.persisted && e(t);}, !0);},c = function (e, t, n) {var i;return function (r) {t.value >= 0 && (r || n) && (t.delta = t.value - (i || 0), (t.delta || void 0 === i) && (i = t.value, e(t)));};},f = -1,s = function () {return "hidden" === document.visibilityState ? 0 : 1 / 0;},m = function () {o(function (e) {var t = e.timeStamp;f = t;}, !0);},v = function () {return f < 0 && (f = s(), m(), u(function () {setTimeout(function () {f = s(), m();}, 0);})), { get firstHiddenTime() {return f;} };},d = function (e, t) {var n,i = v(),o = r("FCP"),f = function (e) {"first-contentful-paint" === e.name && (m && m.disconnect(), e.startTime < i.firstHiddenTime && (o.value = e.startTime, o.entries.push(e), n(!0)));},s = window.performance && performance.getEntriesByName && performance.getEntriesByName("first-contentful-paint")[0],m = s ? null : a("paint", f);(s || m) && (n = c(e, o, t), s && f(s), u(function (i) {o = r("FCP"), n = c(e, o, t), requestAnimationFrame(function () {requestAnimationFrame(function () {o.value = performance.now() - i.timeStamp, n(!0);});});}));},p = !1,l = -1,h = function (e, t) {p || (d(function (e) {l = e.value;}), p = !0);var n,i = function (t) {l > -1 && e(t);},f = r("CLS", 0),s = 0,m = [],v = function (e) {if (!e.hadRecentInput) {var t = m[0],i = m[m.length - 1];s && e.startTime - i.startTime < 1e3 && e.startTime - t.startTime < 5e3 ? (s += e.value, m.push(e)) : (s = e.value, m = [e]), s > f.value && (f.value = s, f.entries = m, n());}},h = a("layout-shift", v);h && (n = c(i, f, t), o(function () {h.takeRecords().map(v), n(!0);}), u(function () {s = 0, l = -1, f = r("CLS", 0), n = c(i, f, t);}));},T = { passive: !0, capture: !0 },y = new Date(),g = function (i, r) {e || (e = r, t = i, n = new Date(), w(removeEventListener), E());},E = function () {if (t >= 0 && t < n - y) {var r = { entryType: "first-input", name: e.type, target: e.target, cancelable: e.cancelable, startTime: e.timeStamp, processingStart: e.timeStamp + t };i.forEach(function (e) {e(r);}), i = [];}},S = function (e) {if (e.cancelable) {var t = (e.timeStamp > 1e12 ? new Date() : performance.now()) - e.timeStamp;"pointerdown" == e.type ? function (e, t) {var n = function () {g(e, t), r();},i = function () {r();},r = function () {removeEventListener("pointerup", n, T), removeEventListener("pointercancel", i, T);};addEventListener("pointerup", n, T), addEventListener("pointercancel", i, T);}(t, e) : g(t, e);}},w = function (e) {["mousedown", "keydown", "touchstart", "pointerdown"].forEach(function (t) {return e(t, S, T);});},L = function (n, f) {var s,m = v(),d = r("FID"),p = function (e) {e.startTime < m.firstHiddenTime && (d.value = e.processingStart - e.startTime, d.entries.push(e), s(!0));},l = a("first-input", p);s = c(n, d, f), l && o(function () {l.takeRecords().map(p), l.disconnect();}, !0), l && u(function () {var a;d = r("FID"), s = c(n, d, f), i = [], t = -1, e = null, w(addEventListener), a = p, i.push(a), E();});},b = {},F = function (e, t) {var n,i = v(),f = r("LCP"),s = function (e) {var t = e.startTime;t < i.firstHiddenTime && (f.value = t, f.entries.push(e), n());},m = a("largest-contentful-paint", s);if (m) {n = c(e, f, t);var d = function () {b[f.id] || (m.takeRecords().map(s), m.disconnect(), b[f.id] = !0, n(!0));};["keydown", "click"].forEach(function (e) {addEventListener(e, d, { once: !0, capture: !0 });}), o(d, !0), u(function (i) {f = r("LCP"), n = c(e, f, t), requestAnimationFrame(function () {requestAnimationFrame(function () {f.value = performance.now() - i.timeStamp, b[f.id] = !0, n(!0);});});});}},P = function (e) {var t,n = r("TTFB");t = function () {try {var t = performance.getEntriesByType("navigation")[0] || function () {var e = performance.timing,t = { entryType: "navigation", startTime: 0 };for (var n in e) "navigationStart" !== n && "toJSON" !== n && (t[n] = Math.max(e[n] - e.navigationStart, 0));return t;}();if (n.value = n.delta = t.responseStart, n.value < 0 || n.value > performance.now()) return;n.entries = [t], e(n);} catch (e) {}}, "complete" === document.readyState ? setTimeout(t, 0) : addEventListener("load", function () {return setTimeout(t, 0);});};export { h as getCLS, d as getFCP, L as getFID, F as getLCP, P as getTTFB };
				
			

We can implement web vitals JS with-

  1. Elementor Pro: Custom Code &
  2. Code Snippet plugin.

Elementor Pro:

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

Step#1: Custom Code

To use Custom Code, make sure you have Elementor Pro 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

How to access Elementor's Custom Code

Step#2: 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.

Click the add new buttob

Add code snippets to your page without writing any php code with Elementor pro custom code

Step#3: Copy and paste the code inside the Elementor’s custom code

				
					/*
* Host web vitals JS locally for better performance
* Get the web-vitals-module.min.js- 
https://foxscribbler.com/wp-content/plugins/global-style/js/web-vitals-module.min.js
* Make sure the script is loaded in JS module 
* Minify the Code
*/
<script type="module">
import {getLCP, getFID, getCLS} from "/wp-content/plugins/global-style/js/web-vitals-module.min.js";

//import {getLCP, getFID, getCLS} from 'https://unpkg.com/web-vitals?module';

function getSelector(node, maxLen = 100) {
    let sel = "";
    try {
        for (; node && 9 !== node.nodeType;) {
            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;
            if (sel = sel ? part + ">" + sel : part, node.id) break;
            node = node.parentNode
        }
    } catch (err) {}
    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) => 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 getDebugInfo(name, entries = []) {
    if (entries.length) {
        if ("LCP" === name) {
            const lastEntry = entries[entries.length - 1];
            return {
                debug_target: getSelector(lastEntry.element),
                event_time: lastEntry.startTime
            }
        }
        if ("FID" === name) {
            const firstEntry = entries[0];
            return {
                debug_target: getSelector(firstEntry.target),
                debug_event: firstEntry.name,
                debug_timing: wasFIDBeforeDCL(firstEntry) ? "pre_dcl" : "post_dcl",
                event_time: firstEntry.startTime
            }
        }
        if ("CLS" === name) {
            const largestEntry = getLargestLayoutShiftEntry(entries);
            if (largestEntry && largestEntry.sources && largestEntry.sources.length) {
                const largestSource = getLargestLayoutShiftSource(largestEntry.sources);
                if (largestSource) return {
                    debug_target: getSelector(largestSource.node),
                    event_time: largestEntry.startTime
                }
            }
        }
    }
    return {
        debug_target: "(not set)"
    }
}
//Sending events to GA4
function sendToGoogleAnalytics({
    name: name,
    delta: delta,
    value: value,
    id: id,
    entries: entries
}) {
    gtag("event", name, {
        value: delta,
        metric_id: id,
        metric_value: value,
        metric_delta: delta,
        ...getDebugInfo(name, entries)
    })
}
getLCP(sendToGoogleAnalytics), 
getFID(sendToGoogleAnalytics), 
getCLS(sendToGoogleAnalytics);
</script>
				
			

Step#4: 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

Display Condition

Code Snippets Plugins:

Step#1: 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

Step#2: Click the Add new:

Add new Snippet

Step#3: Deploy the new snippet with PHP

Instead of inlining the JS to the head or body open, you can combine the JS into a script tag.

				
					/*
*You can combine the JS 
*/
function web_vitals() {
?>
    <script type="module">
        //Host web vitals JS locally for better performance
    // Get the web-vitals-module.min.js- https://foxscribbler.com/wp-content/plugins/global-style/js/web-vitals-module.min.js

    import {
        getLCP,
        getFID,
        getCLS
    } from "/wp-content/plugins/global-style/js/web-vitals-module.min.js";

    //import {getLCP, getFID, getCLS} from 'https://unpkg.com/web-vitals?module';

    function getSelector(node, maxLen = 100) {
        let sel = "";
        try {
            for (; node && 9 !== node.nodeType;) {
                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;
                if (sel = sel ? part + ">" + sel : part, node.id) break;
                node = node.parentNode
            }
        } catch (err) {}
        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) => 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 getDebugInfo(name, entries = []) {
        if (entries.length) {
            if ("LCP" === name) {
                const lastEntry = entries[entries.length - 1];
                return {
                    debug_target: getSelector(lastEntry.element),
                    event_time: lastEntry.startTime
                }
            }
            if ("FID" === name) {
                const firstEntry = entries[0];
                return {
                    debug_target: getSelector(firstEntry.target),
                    debug_event: firstEntry.name,
                    debug_timing: wasFIDBeforeDCL(firstEntry) ? "pre_dcl" : "post_dcl",
                    event_time: firstEntry.startTime
                }
            }
            if ("CLS" === name) {
                const largestEntry = getLargestLayoutShiftEntry(entries);
                if (largestEntry && largestEntry.sources && largestEntry.sources.length) {
                    const largestSource = getLargestLayoutShiftSource(largestEntry.sources);
                    if (largestSource) return {
                        debug_target: getSelector(largestSource.node),
                        event_time: largestEntry.startTime
                    }
                }
            }
        }
        return {
            debug_target: "(not set)"
        }
    }
    //Sending events to GA4
    function sendToGoogleAnalytics({
        name: name,
        delta: delta,
        value: value,
        id: id,
        entries: entries
    }) {
        gtag("event", name, {
            value: delta,
            metric_id: id,
            metric_value: value,
            metric_delta: delta,
            ...getDebugInfo(name, entries)
        })
    }
    getLCP(sendToGoogleAnalytics), 
    getFID(sendToGoogleAnalytics), 
    getCLS(sendToGoogleAnalytics);
</script>

<?php

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

Save the snippet and clear the cache.

Core Web Vitals Report

After a few days, you will be able to see the graphs. It depends on your traffic.

Core Web Vitals Report summary

Page Path Analysis

Want to dig deeper into the Core Web Vitals report, you can access my report from Data Studio

Reference

Here are the external resources, you can rely on when it comes to Core web Vitals.

  1. Official Web Vitals.
  2. Core Web Vitals YouTube Playlist.
  3. Performance from By HTTP Archive
  4. Core Web Vitals & Page Experience FAQs (Updated: March 2021).
  5. Browser Support, Polyfills for Web Vitals JS libraries
  6. 3rd parties RUM Analytics and Providers
  7. Interval of the CrUX dataset on BigQuery

Need Assistance

If you need help setting up Web Vitals JS libraries with Google Analytics 4 or help you pass your Core Web Vitals Assessments.

  1. Contact me on Facebook
  2. Hire me on Elementor Experts Network.
  3. Send me a message on Telegram.

CrUX report – Collapse View

CrUX report - Collapse view

CrUX report – Toggle View (at 75th percentile)

CrUX report - toggle view

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.