Core Web Vitals + Measurement
Your Lighthouse score is 98 but Google Search Console shows "Poor" Core Web Vitals. Your site dropped in rankings and you don't know why.
Here's the disconnect: Lighthouse measures lab data (synthetic tests), but Google ranks based on field data (real users over 28 days). That 98 score means nothing if your actual users on 3G mobile are experiencing 4-second LCP. Time to measure what actually matters.
Outcome
Vitals instrumentation with Vercel Speed Insights or custom reporting. Track LCP < 2.5s, INP < 200ms, CLS < 0.1 targets. Real user data (not synthetic Lighthouse scores) informs performance improvements.
Fast Track
- Add
@vercel/speed-insightsfor real-time Core Web Vitals tracking. - Monitor LCP (loading), INP (interactivity), CLS (visual stability) in production.
- Use field data (real user measurements) to identify bottlenecks - Google only ranks based on real user data, not lab data (synthetic measurements from testing tools like Lighthouse).
Hands-On Exercise 3.6
Requirements:
- Install
@vercel/speed-insightsand@vercel/analyticsfor real-time vitals tracking. - Capture LCP (Largest Contentful Paint), INP (Interaction to Next Paint), CLS (Cumulative Layout Shift).
- Set up custom reporting endpoint to log vitals to your analytics system.
- Document thresholds: LCP < 2.5s, INP < 200ms, CLS < 0.1.
- Track improvements after image/font/rendering optimizations.
Implementation hints:
- Google uses field data only for ranking - real user data from Chrome over 28-day window.
- Field data = 75th percentile of users over last 28 days.
- Lighthouse scores don't affect SEO - only real user Core Web Vitals do.
- Vercel Speed Insights shows real-time data - Google takes 28 days to reflect changes.
- Core Web Vitals are UX metrics - use them to debug performance issues, not just chase rankings.
- Mobile and desktop tracked separately.
- Only "sufficiently popular" and "publicly discoverable" pages tracked by Google.
Try It
-
Monitor real-time vitals:
- Deploy to Vercel with Speed Insights enabled.
- Visit production site, interact with page.
- Check Vercel dashboard for LCP, INP, CLS metrics.
-
Test vitals locally:
- Open DevTools Console, look for web-vitals logs.
- Use Lighthouse Performance tab for lab data (not used for ranking).
- Compare field data (real users) vs lab data (synthetic).
-
Verify tracking:
- Trigger interaction (click, type) and verify INP captured.
- Scroll page and verify CLS remains < 0.1.
- Check LCP element in Performance tab - should be hero image or main content.
Commit & Deploy
git add -A
git commit -m "feat(advanced): add core web vitals measurement"
git push -u origin feat/advanced-vitalsDone-When
instrumentation-client.tsexists inapps/web/src/and importsregisterWebVitals- Open DevTools Console on any page: see "LCP:", "INP:", and "CLS:" log entries with numeric values
- LCP value logged is < 2500ms (green zone) or identifies specific improvement needed
- INP value logged is < 200ms after clicking a button or typing in an input
- CLS value logged is < 0.1 after page fully loads and stabilizes
- Open Vercel dashboard → Speed Insights: see real user Core Web Vitals data appearing
- Run Lighthouse in DevTools → Performance tab: LCP element identified in "Largest Contentful Paint element" section
Solution
Click to reveal solution
apps/web/src/app/layout.tsx
import { SpeedInsights } from '@vercel/speed-insights/next'
import { Analytics } from '@vercel/analytics/react'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
{children}
<Analytics />
<SpeedInsights />
</body>
</html>
)
}apps/web/src/lib/web-vitals.ts
import { onCLS, onINP, onLCP } from 'web-vitals'
export function registerWebVitals() {
onLCP((metric) => {
console.log('LCP:', metric.value, 'ms', 'Target: <2500ms')
sendToAnalytics(metric)
})
onINP((metric) => {
console.log('INP:', metric.value, 'ms', 'Target: <200ms')
sendToAnalytics(metric)
})
onCLS((metric) => {
console.log('CLS:', metric.value, 'Target: <0.1')
sendToAnalytics(metric)
})
}
function sendToAnalytics(metric: any) {
const { id, name, value, rating } = metric
// Send to your analytics endpoint
fetch('/api/analytics/vitals', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id,
name,
value,
rating,
url: window.location.pathname,
timestamp: Date.now(),
}),
}).catch((error) => {
console.error('Failed to send vitals:', error)
})
}apps/web/src/instrumentation-client.ts
import { registerWebVitals } from './lib/web-vitals'
// Initialize web vitals tracking before React hydration
// This ensures we capture metrics from the earliest possible moment
registerWebVitals()apps/web/src/app/api/analytics/vitals/route.ts
import { NextResponse } from 'next/server'
export async function POST(request: Request) {
try {
const vitals = await request.json()
// Log to your analytics system (e.g., database, external service)
console.log('Core Web Vitals:', vitals)
// Store in database or forward to analytics service
// await db.vitals.create({ data: vitals })
return NextResponse.json({ success: true })
} catch (error) {
return NextResponse.json({ error: 'Failed to record vitals' }, { status: 500 })
}
}Install dependencies
pnpm add @vercel/speed-insights @vercel/analytics web-vitalsReferences
Was this helpful?