Measuring & Monitoring Core Web Vitals
Tools and techniques for measuring, monitoring, and improving Core Web Vitals
Effective performance optimization requires accurate measurement. This guide covers the tools and techniques for measuring Core Web Vitals in both development and production.
Lab vs Field Data
Understanding the Difference
| Type | Source | Pros | Cons |
|---|---|---|---|
| Lab Data | Lighthouse, DevTools | Reproducible, detailed, immediate | Doesn't reflect real users |
| Field Data | CrUX, RUM | Real user experience, diverse conditions | Delayed, less detailed |
Google Search Console uses field data (CrUX) for ranking decisions. Lab data is for debugging, not for SEO assessment.
Setting Up web-vitals
Recommended File Structure
Basic Setup
npm install web-vitalsimport { onCLS, onINP, onLCP, onFCP, onTTFB } from 'web-vitals';
import type { Metric } from 'web-vitals';
function sendToAnalytics(metric: Metric) {
const body = JSON.stringify({
name: metric.name,
value: metric.value,
rating: metric.rating,
delta: metric.delta,
id: metric.id,
navigationType: metric.navigationType,
});
// Use sendBeacon for reliability
if (navigator.sendBeacon) {
navigator.sendBeacon('/api/analytics', body);
} else {
fetch('/api/analytics', {
body,
method: 'POST',
keepalive: true,
});
}
}
export function reportWebVitals() {
onCLS(sendToAnalytics);
onINP(sendToAnalytics);
onLCP(sendToAnalytics);
onFCP(sendToAnalytics);
onTTFB(sendToAnalytics);
}Integration with Next.js
'use client';
import { useEffect } from 'react';
import { reportWebVitals } from '@/lib/web-vitals';
export default function RootLayout({ children }) {
useEffect(() => {
reportWebVitals();
}, []);
return (
<html>
<body>{children}</body>
</html>
);
}With Attribution for Debugging
import {
onCLS,
onINP,
onLCP,
type CLSMetricWithAttribution,
type INPMetricWithAttribution,
type LCPMetricWithAttribution,
} from 'web-vitals/attribution';
export function reportWithAttribution() {
onLCP((metric: LCPMetricWithAttribution) => {
console.log('LCP Debug:', {
value: metric.value,
element: metric.attribution.element,
url: metric.attribution.url,
timeToFirstByte: metric.attribution.timeToFirstByte,
resourceLoadDelay: metric.attribution.resourceLoadDelay,
resourceLoadDuration: metric.attribution.resourceLoadDuration,
elementRenderDelay: metric.attribution.elementRenderDelay,
});
});
onINP((metric: INPMetricWithAttribution) => {
console.log('INP Debug:', {
value: metric.value,
interactionTarget: metric.attribution.interactionTarget,
interactionType: metric.attribution.interactionType,
inputDelay: metric.attribution.inputDelay,
processingDuration: metric.attribution.processingDuration,
presentationDelay: metric.attribution.presentationDelay,
});
});
onCLS((metric: CLSMetricWithAttribution) => {
console.log('CLS Debug:', {
value: metric.value,
largestShiftTarget: metric.attribution.largestShiftTarget,
largestShiftValue: metric.attribution.largestShiftValue,
});
});
}Chrome DevTools
Performance Panel
Open DevTools Press F12 or Cmd+Option+I
Go to Performance tab Select the Performance panel
Configure settings Enable CPU throttling (4x slowdown) and Network throttling (Fast 3G)
Record page load Click Record → Refresh page → Stop recording
Analyze results Look at Timings row for LCP, CLS markers
Lighthouse
# CLI usage
npx lighthouse https://example.com --view
# With specific categories
npx lighthouse https://example.com \
--only-categories=performance \
--output=json \
--output-path=./report.jsonCore Web Vitals Overlay
// Enable in development
// Add to your root layout or _app.tsx
if (process.env.NODE_ENV === 'development') {
import('web-vitals').then(({ onCLS, onINP, onLCP }) => {
const overlay = document.createElement('div');
overlay.style.cssText = `
position: fixed;
bottom: 20px;
right: 20px;
background: #1a1a1a;
color: white;
padding: 12px;
border-radius: 8px;
font-family: monospace;
font-size: 12px;
z-index: 99999;
`;
document.body.appendChild(overlay);
const metrics = { LCP: '-', INP: '-', CLS: '-' };
function updateOverlay() {
overlay.innerHTML = Object.entries(metrics)
.map(([name, value]) => `${name}: ${value}`)
.join('<br>');
}
onLCP((m) => { metrics.LCP = `${m.value.toFixed(0)}ms`; updateOverlay(); });
onINP((m) => { metrics.INP = `${m.value.toFixed(0)}ms`; updateOverlay(); });
onCLS((m) => { metrics.CLS = m.value.toFixed(3); updateOverlay(); });
updateOverlay();
});
}Real User Monitoring (RUM)
Vercel Analytics
import { Analytics } from '@vercel/analytics/react';
import { SpeedInsights } from '@vercel/speed-insights/next';
export default function Layout({ children }) {
return (
<html>
<body>
{children}
<Analytics />
<SpeedInsights />
</body>
</html>
);
}Custom RUM Implementation
interface PerformanceData {
url: string;
userAgent: string;
connection?: string;
deviceMemory?: number;
metrics: {
name: string;
value: number;
rating: string;
}[];
}
class RUM {
private buffer: PerformanceData['metrics'] = [];
private sent = false;
constructor() {
// Send data when page is hidden
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
this.flush();
}
});
}
addMetric(metric: { name: string; value: number; rating: string }) {
this.buffer.push(metric);
}
private flush() {
if (this.sent || this.buffer.length === 0) return;
this.sent = true;
const data: PerformanceData = {
url: window.location.href,
userAgent: navigator.userAgent,
connection: (navigator as any).connection?.effectiveType,
deviceMemory: (navigator as any).deviceMemory,
metrics: this.buffer,
};
navigator.sendBeacon('/api/rum', JSON.stringify(data));
}
}
export const rum = new RUM();PageSpeed Insights API
const API_KEY = process.env.PAGESPEED_API_KEY;
interface PSIResult {
lighthouseResult: {
audits: {
'largest-contentful-paint': { numericValue: number };
'cumulative-layout-shift': { numericValue: number };
'total-blocking-time': { numericValue: number };
};
};
loadingExperience: {
metrics: {
LARGEST_CONTENTFUL_PAINT_MS: { percentile: number };
CUMULATIVE_LAYOUT_SHIFT_SCORE: { percentile: number };
INTERACTION_TO_NEXT_PAINT: { percentile: number };
};
};
}
async function checkPerformance(url: string): Promise<void> {
const apiUrl = new URL('https://www.googleapis.com/pagespeedonline/v5/runPagespeed');
apiUrl.searchParams.set('url', url);
apiUrl.searchParams.set('key', API_KEY!);
apiUrl.searchParams.set('strategy', 'mobile');
apiUrl.searchParams.set('category', 'performance');
const response = await fetch(apiUrl);
const data: PSIResult = await response.json();
console.log('Lab Data (Lighthouse):');
console.log(' LCP:', data.lighthouseResult.audits['largest-contentful-paint'].numericValue);
console.log(' CLS:', data.lighthouseResult.audits['cumulative-layout-shift'].numericValue);
console.log(' TBT:', data.lighthouseResult.audits['total-blocking-time'].numericValue);
if (data.loadingExperience?.metrics) {
console.log('\nField Data (CrUX):');
console.log(' LCP:', data.loadingExperience.metrics.LARGEST_CONTENTFUL_PAINT_MS.percentile);
console.log(' CLS:', data.loadingExperience.metrics.CUMULATIVE_LAYOUT_SHIFT_SCORE.percentile);
console.log(' INP:', data.loadingExperience.metrics.INTERACTION_TO_NEXT_PAINT.percentile);
}
}CI/CD Integration
Lighthouse CI
name: Lighthouse CI
on: [push]
jobs:
lighthouse:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- run: npm ci && npm run build
- name: Run Lighthouse
uses: treosh/lighthouse-ci-action@v10
with:
urls: |
https://example.com/
https://example.com/products
budgetPath: ./lighthouse-budget.json
uploadArtifacts: true[
{
"path": "/*",
"timings": [
{ "metric": "largest-contentful-paint", "budget": 2500 },
{ "metric": "first-contentful-paint", "budget": 1800 },
{ "metric": "cumulative-layout-shift", "budget": 0.1 },
{ "metric": "total-blocking-time", "budget": 200 }
]
}
]Performance Budget Alerts
interface Budget {
LCP: number;
INP: number;
CLS: number;
}
const BUDGET: Budget = {
LCP: 2500,
INP: 200,
CLS: 0.1,
};
async function checkAgainstBudget(metrics: Record<string, number>) {
const failures: string[] = [];
if (metrics.LCP > BUDGET.LCP) {
failures.push(`LCP ${metrics.LCP}ms exceeds budget of ${BUDGET.LCP}ms`);
}
if (metrics.INP > BUDGET.INP) {
failures.push(`INP ${metrics.INP}ms exceeds budget of ${BUDGET.INP}ms`);
}
if (metrics.CLS > BUDGET.CLS) {
failures.push(`CLS ${metrics.CLS} exceeds budget of ${BUDGET.CLS}`);
}
if (failures.length > 0) {
console.error('Performance budget exceeded:');
failures.forEach(f => console.error(` - ${f}`));
process.exit(1);
}
console.log('All metrics within budget!');
}Monitoring Dashboard
Data Structure for Analytics
interface PerformanceEntry {
timestamp: Date;
url: string;
metrics: {
lcp: number;
inp: number;
cls: number;
fcp: number;
ttfb: number;
};
context: {
deviceType: 'mobile' | 'tablet' | 'desktop';
connectionType: string;
country: string;
browser: string;
};
}
// Calculate p75 for each metric
function calculateP75(entries: PerformanceEntry[], metric: keyof PerformanceEntry['metrics']): number {
const values = entries.map(e => e.metrics[metric]).sort((a, b) => a - b);
const index = Math.floor(values.length * 0.75);
return values[index];
}Measurement Checklist
Development
Set up web-vitals package Install and configure the official web-vitals library.
Enable attribution build for debugging
Use web-vitals/attribution for detailed breakdowns.
Use Chrome DevTools Performance panel Profile real interactions and page loads.
Test with CPU/Network throttling Simulate slower devices and connections.
Check Lighthouse scores regularly Run audits during development.
Production
Implement RUM (Vercel Analytics or custom) Collect real user metrics from production.
Set up alerting for regression Get notified when metrics degrade.
Monitor p75 values weekly The 75th percentile is what Google uses.
Track by page, device type, country Identify specific problem areas.
Compare lab vs field data Lab is for debugging; field is the truth.
CI/CD
Add Lighthouse CI to pipeline Automate performance testing on every PR.
Define performance budgets Set thresholds for each metric.
Block PRs that exceed budgets Prevent performance regressions from shipping.
Track trends over time Monitor for gradual degradation.
Alert on significant regressions Catch problems before users notice.
Tools Summary
| Tool | Type | Best For |
|---|---|---|
| Chrome DevTools | Lab | Debugging, profiling |
| Lighthouse | Lab | Comprehensive audits |
| PageSpeed Insights | Both | Quick checks with field data |
| Search Console | Field | Site-wide CWV status |
| web-vitals | Field | Custom RUM |
| Vercel Analytics | Field | Next.js apps on Vercel |
| Lighthouse CI | Lab | CI/CD integration |