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
The web-vitals library approach
Section titled “The web-vitals library approach”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.
Loading via Custom HTML tag
Section titled “Loading via Custom HTML tag”<!-- 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:
- In GTM, go to Tags → New → Discover more tag types in the Community Template Gallery.
- Search for “Core Web Vitals”.
- Install the template (it will ask you to review and accept permissions).
- Configure it — set your measurement thresholds and which metrics to track.
- Set the trigger to Window Loaded on all pages.
- Create a GA4 Event tag triggered by the
core_web_vitalcustom event this template fires.
GTM tag configuration
Section titled “GTM tag configuration”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
GA4 custom dimensions
Section titled “GA4 custom dimensions”To use these metrics in GA4 reports and Explorations, register custom dimensions:
In GA4 → Configure → Custom Definitions → Create custom dimension:
metric_name— event-scopedmetric_value— event-scopedmetric_rating— event-scoped
Once registered, these appear in Explorations within 24-48 hours.
BigQuery analysis
Section titled “BigQuery analysis”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 daysSELECT (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_goodFROM `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_pathHAVING COUNT(*) > 100ORDER BY avg_lcp_ms DESCLIMIT 20GTM’s own impact on Core Web Vitals
Section titled “GTM’s own impact on Core Web Vitals”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
Common mistakes
Section titled “Common mistakes”Not accounting for CLS finalization
Section titled “Not accounting for CLS finalization”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.