Skip to content

Core Web Vitals

Core Web Vitals are Google’s set of metrics that measure real user experience: loading performance, interactivity, and visual stability. They directly affect your Search ranking. Measuring them via GTM lets you capture field data — actual user experiences — and send it to GA4 where you can segment it by page, device, and traffic source.

The three metrics:

  • LCP (Largest Contentful Paint) — How long until the main content loads. Target: under 2.5 seconds.
  • INP (Interaction to Next Paint) — Responsiveness to user interactions. Target: under 200ms.
  • CLS (Cumulative Layout Shift) — Visual stability — how much the layout shifts unexpectedly. Target: under 0.1.

Why measure via GTM instead of using CrUX?

Section titled “Why measure via GTM instead of using CrUX?”

Google’s Chrome User Experience Report (CrUX) provides aggregate Core Web Vitals data free in Search Console and PageSpeed Insights. For many sites, CrUX is sufficient.

You need GTM-based measurement when you want to:

  • Segment by GA4 dimensions — LCP by traffic source, device category, user type, A/B test variant
  • Correlate with business metrics — Do users with poor INP convert less?
  • Debug specific pages — CrUX is anonymous aggregate data; GA4 has page-level detail
  • Track improvements over time in your own analytics alongside other KPIs

Google’s web-vitals JavaScript library is the standard implementation. It handles browser compatibility, timing edge cases, and the finalization behavior of each metric correctly.

<!-- Custom HTML tag: "CWV - Load web-vitals Library"
Fires on: Window Loaded trigger -->
<script>
(function() {
// Load web-vitals from a CDN (or host it yourself)
var script = document.createElement('script');
script.src = 'https://unpkg.com/web-vitals@3/dist/web-vitals.attribution.iife.js';
script.onload = function() {
// web-vitals global is now available as webVitals
var vitals = window.webVitals;
if (!vitals) return;
function sendToDataLayer(metric) {
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
event: 'core_web_vital',
metric_name: metric.name, // LCP, INP, CLS, FCP, TTFB
metric_value: Math.round(metric.name === 'CLS' ? metric.value * 1000 : metric.value),
metric_rating: metric.rating, // 'good', 'needs-improvement', 'poor'
metric_delta: metric.delta,
metric_id: metric.id,
metric_navigationType: metric.navigationType
});
}
vitals.onLCP(sendToDataLayer);
vitals.onINP(sendToDataLayer);
vitals.onCLS(sendToDataLayer);
vitals.onFCP(sendToDataLayer);
vitals.onTTFB(sendToDataLayer);
};
document.head.appendChild(script);
})();
</script>

The Simo Ahava Community Template approach

Section titled “The Simo Ahava Community Template approach”

There is a well-maintained Core Web Vitals template in the GTM Community Template Gallery by Simo Ahava and Charles Bordons. It handles library loading, dataLayer pushes, and GA4 event configuration without requiring any custom HTML.

To install it:

  1. In GTM, go to Tags → New → Discover more tag types in the Community Template Gallery.
  2. Search for “Core Web Vitals”.
  3. Install the template (it will ask you to review and accept permissions).
  4. Configure it — set your measurement thresholds and which metrics to track.
  5. Set the trigger to Window Loaded on all pages.
  6. Create a GA4 Event tag triggered by the core_web_vital custom event this template fires.

Create a GA4 Event tag:

  • Event Name: core_web_vital
  • Trigger: Custom Event trigger for core_web_vital
  • Parameters:
    • metric_name{{DLV - metric_name}}
    • metric_value{{DLV - metric_value}}
    • metric_rating{{DLV - metric_rating}}
    • page_path{{Page Path}}
    • device_category → your device detection variable

To use these metrics in GA4 reports and Explorations, register custom dimensions:

In GA4 → Configure → Custom Definitions → Create custom dimension:

  • metric_name — event-scoped
  • metric_value — event-scoped
  • metric_rating — event-scoped

Once registered, these appear in Explorations within 24-48 hours.

If you export GA4 to BigQuery, Core Web Vitals data becomes queryable alongside all other event data:

-- Average LCP by page path for the last 7 days
SELECT
(SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'page_path') AS page_path,
ROUND(AVG(
(SELECT value.int_value FROM UNNEST(event_params) WHERE key = 'metric_value')
), 0) AS avg_lcp_ms,
COUNTIF(
(SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'metric_rating') = 'good'
) / COUNT(*) * 100 AS pct_good
FROM `your_project.analytics_XXXXXXXXXX.events_*`
WHERE
_TABLE_SUFFIX BETWEEN FORMAT_DATE('%Y%m%d', DATE_SUB(CURRENT_DATE(), INTERVAL 7 DAY))
AND FORMAT_DATE('%Y%m%d', CURRENT_DATE())
AND event_name = 'core_web_vital'
AND (SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'metric_name') = 'LCP'
GROUP BY page_path
HAVING COUNT(*) > 100
ORDER BY avg_lcp_ms DESC
LIMIT 20

GTM itself adds overhead:

  • The container script request adds network latency
  • Tags that load third-party scripts can block the main thread
  • Large containers with many tags evaluated on every event increase TBT (Total Blocking Time)

Measuring your CWV through GTM creates an inherent measurement bias — you’re measuring the page with GTM loaded. To isolate GTM’s impact, compare CrUX data (which is the real user distribution) with your lab measurements with and without GTM.

Practical ways to reduce GTM’s CWV impact:

  • Load marketing pixels (Meta, TikTok) on Window Loaded instead of Page View
  • Use server-side GTM to offload third-party requests from the browser
  • Audit your container — remove any tags firing scripts on page load that aren’t needed immediately

CLS accumulates over the page lifetime. The web-vitals library fires the final CLS value when the user leaves the page (via visibilitychange). This means the CLS event may not arrive in GA4 if the user closes the tab before GTM fires it. Use navigator.sendBeacon() for CLS delivery, or accept the measurement gap.

Sending raw CLS values to integer dimensions

Section titled “Sending raw CLS values to integer dimensions”

CLS values like 0.083 will be stored as 0 in an integer GA4 dimension. Multiply by 1000 before sending, and document the conversion.

Tracking without GA4 custom dimensions registered

Section titled “Tracking without GA4 custom dimensions registered”

You can collect the events, but they won’t appear as filterable dimensions in GA4 reports until you register the custom dimensions. Do this before you start relying on the data.