Track Web Vitals
Core Web Vitals measure real user experience: how fast pages appear to load (LCP), how responsive they are to interaction (INP), and how much layout shifts happen during load (CLS). Google Search uses these scores in its ranking algorithm. Tracking them in GA4 means you can correlate page performance with conversion rates and identify which pages need performance work.
The three metrics
Section titled “The three metrics”| Metric | What it measures | Good | Needs Improvement | Poor |
|---|---|---|---|---|
| LCP | Largest Contentful Paint — when the main content is visible | < 2.5s | 2.5–4s | > 4s |
| INP | Interaction to Next Paint — responsiveness to all interactions | < 200ms | 200–500ms | > 500ms |
| CLS | Cumulative Layout Shift — visual stability | < 0.1 | 0.1–0.25 | > 0.25 |
Implementation using the web-vitals library
Section titled “Implementation using the web-vitals library”Google’s web-vitals library is the standard way to measure these metrics. It handles all the complexity of the Intersection Observer, PerformanceObserver, and event timing APIs.
Pushes each Core Web Vital as it is measured. LCP fires after the page loads, CLS and INP fire when the user navigates away.
(function() { // Load the web-vitals library from a CDN // In production, self-host or bundle this var script = document.createElement('script'); script.src = 'https://unpkg.com/web-vitals@4/dist/web-vitals.iife.js'; script.onload = function() { function sendVital(metric) { window.dataLayer = window.dataLayer || []; window.dataLayer.push({ event: '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: Math.round(metric.delta), metric_id: metric.id, navigation_type: metric.navigationType, page_path: window.location.pathname }); }
webVitals.onLCP(sendVital); webVitals.onINP(sendVital); webVitals.onCLS(sendVital); webVitals.onFCP(sendVital); // First Contentful Paint webVitals.onTTFB(sendVital); // Time to First Byte }; document.head.appendChild(script);})();-
Add the code as a Custom HTML tag in GTM
- Trigger: Window Loaded (All Pages)
-
Create a Custom Event Trigger
- Trigger type: Custom Event
- Event name:
web_vital
-
Create Data Layer Variables
DLV - metric_name→metric_nameDLV - metric_value→metric_valueDLV - metric_rating→metric_ratingDLV - navigation_type→navigation_type
-
Create a GA4 Event Tag
- Event name:
web_vital - Parameters:
metric_name→{{DLV - metric_name}}metric_value→{{DLV - metric_value}}metric_rating→{{DLV - metric_rating}}navigation_type→{{DLV - navigation_type}}page_path→{{Page Path}}
- Trigger: the Custom Event trigger
- Event name:
-
Register custom dimensions in GA4
Go to GA4 → Admin → Custom Definitions → Custom Dimensions:
metric_name: Event scopemetric_value: Event scope (type: Number)metric_rating: Event scope
-
Test in Preview Mode
Visit a page. Within a few seconds,
web_vitalevents should start appearing — FCP and TTFB first, then LCP. CLS and INP will only fire when the user navigates away or the tab becomes hidden.
GA4 - web_vital
- Type
- Google Analytics: GA4 Event
- Trigger
- Custom Event - web_vital
- Variables
-
DLV - metric_nameDLV - metric_valueDLV - metric_rating
Analysing Web Vitals data in GA4
Section titled “Analysing Web Vitals data in GA4”Create an Exploration report:
- Dimensions:
metric_name,page_path,metric_rating - Metric:
Event count - Filter:
Event name = web_vital
This shows the distribution of ratings (good/needs-improvement/poor) per metric per page.
To find your worst-performing pages:
- Filter:
metric_name = LCPANDmetric_rating = poor - Dimension:
page_path - Sort by:
Event countdescending
BigQuery analysis query
Section titled “BigQuery analysis query”If you have the GA4 BigQuery export enabled:
SELECT params.value.string_value AS page_path, metric_params.value.string_value AS metric_name, AVG(value_params.value.int_value) / CASE metric_params.value.string_value WHEN 'CLS' THEN 1000.0 ELSE 1.0 END AS avg_metric_value, COUNTIF(rating_params.value.string_value = 'good') / COUNT(*) AS pct_good, COUNT(*) AS total_measurementsFROM `your_project.your_dataset.events_*`, UNNEST(event_params) AS params, UNNEST(event_params) AS metric_params, UNNEST(event_params) AS value_params, UNNEST(event_params) AS rating_paramsWHERE _TABLE_SUFFIX BETWEEN '20240101' AND '20240131' AND event_name = 'web_vital' AND params.key = 'page_path' AND metric_params.key = 'metric_name' AND value_params.key = 'metric_value' AND rating_params.key = 'metric_rating'GROUP BY 1, 2ORDER BY pct_good ASCLIMIT 50Test it
Section titled “Test it”- Open GTM Preview and visit a content page
- Wait 3-5 seconds — FCP, TTFB, and LCP events should appear in the Summary pane
- Navigate to another page (or close the tab) — CLS and INP events should fire on the original page
- Check
metric_valueandmetric_ratingare sensible (LCP should be in the hundreds of milliseconds for a fast page) - In GA4 DebugView, verify
web_vitalevents appear with parameters
Common gotchas
Section titled “Common gotchas”INP replaces FID. The web-vitals library v3+ measures INP (Interaction to Next Paint) instead of FID (First Input Delay). If you have existing FID data in GA4, they are not directly comparable.
CLS fires multiple times. The web-vitals library reports CLS as the user interacts with the page. Each time a new layout shift exceeds the previous maximum CLS value, a new event is fired. The final CLS value (when the user leaves the page) is the authoritative one.
CDN loading adds latency. Loading web-vitals from unpkg.com adds a network request. For production, bundle the library into your site code or self-host it.