Core Web Vitals monitoring (LCP, FID, CLS, INP, TTFB), measurement with web-vitals library, reporting to analytics, and optimization strategies for Next.js
Core Web Vitals monitoring (LCP, FID, CLS, INP, TTFB), measurement with web-vitals library, reporting to analytics, and optimization strategies for Next.js
layer
domain
category
performance
triggers
["web vitals","core web vitals","LCP","CLS","INP","TTFB","FID","page speed","lighthouse","performance score","largest contentful paint","cumulative layout shift","interaction to next paint"]
inputs
["Application URL or codebase","Current performance metrics or Lighthouse scores","Target performance budgets"]
Measure, monitor, and optimize Core Web Vitals -- the user-centric performance metrics that Google uses for search ranking. This skill covers the web-vitals library, real-user monitoring (RUM), lab testing, and concrete optimization strategies for each metric.
// app/api/vitals/route.tsimport { NextRequest, NextResponse } from"next/server";
exportasyncfunctionPOST(request: NextRequest) {
const metric = await request.json();
// Log for monitoring (replace with your analytics pipeline)console.log(`[Web Vital] ${metric.name}: ${metric.value} (${metric.rating})`);
// Forward to your analytics service// await analytics.track("web_vital", metric);returnNextResponse.json({ ok: true });
}
3. LCP Optimization
// Priority hints for LCP element// In Next.js, use priority prop on the LCP imageimportImagefrom"next/image";
exportfunctionHeroSection() {
return (
<section>
{/* priority prop adds fetchpriority="high" and preloads the image */}
<Imagesrc="/hero.webp"alt="Hero banner"width={1200}height={600}prioritysizes="100vw"
/><h1>Welcome</h1></section>
);
}
// Break up long tasks with yield-to-main patternsfunctionyieldToMain(): Promise<void> {
returnnewPromise((resolve) => {
// scheduler.yield() is the modern API (Chrome 129+)if ("scheduler"in globalThis && "yield"in (globalThis asany).scheduler) {
(globalThis asany).scheduler.yield().then(resolve);
} else {
setTimeout(resolve, 0);
}
});
}
// Use in long-running event handlersasyncfunctionhandleExpensiveClick(items: Item[]) {
for (let i = 0; i < items.length; i++) {
processItem(items[i]);
// Yield every 50 items to keep INP lowif (i % 50 === 0 && i > 0) {
awaityieldToMain();
}
}
}
// budget.json (Lighthouse CI budget)[{"path":"/*","timings":[{"metric":"largest-contentful-paint","budget":2500},{"metric":"cumulative-layout-shift","budget":0.1},{"metric":"interactive","budget":3500},{"metric":"first-contentful-paint","budget":1800}],"resourceSizes":[{"resourceType":"script","budget":150},{"resourceType":"stylesheet","budget":50},{"resourceType":"total","budget":500}]}]
Best Practices
Measure in the field, not just in the lab -- Lighthouse (lab) gives a starting point, but RUM data from web-vitals reflects real user experience
Use the 75th percentile -- Google evaluates Web Vitals at the 75th percentile of page loads, not the average
Prioritize the LCP resource -- Use fetchpriority="high", preload, and avoid lazy-loading the LCP element
Avoid layout shifts from web fonts -- Use font-display: optional or swap with proper fallback metrics
Defer non-critical JavaScript -- Use dynamic imports, next/dynamic, or <script defer> for non-essential code
Use CSS containment -- contain: layout prevents layout recalculations from propagating
Minimize main-thread work -- Offload heavy computation to Web Workers, and yield to main between tasks
Optimize server response time -- Use edge rendering, streaming SSR, and CDN caching to reduce TTFB
Set explicit sizes on all images, videos, iframes, and ad slots to prevent CLS
Test on real devices -- Use Chrome DevTools throttling or real mobile devices, not just fast desktops
Common Pitfalls
Pitfall
Impact
Fix
Lazy-loading the LCP image
LCP delayed by intersection observer
Use priority prop or fetchpriority="high"
Injecting content above the fold after load
CLS spike
Reserve space or use CSS contain
Large synchronous event handlers
Poor INP
Break up work, use startTransition, yield to main
Third-party scripts blocking render
LCP and INP degradation
Load with async/defer, use Partytown for heavy scripts
1. Install: npm install web-vitals
2. Create lib/web-vitals.ts with measurement + sendBeacon reporting
3. Create WebVitalsReporter client component, mount in root layout
4. Create /api/vitals endpoint to receive and forward metrics
5. Set up Lighthouse CI in GitHub Actions for PR checks
6. Configure performance budgets in budget.json
7. Monitor field data in analytics dashboard
Example 2: Optimizing a Slow Product Page
Problem: LCP = 4.2s, CLS = 0.35, INP = 450ms
LCP Fix:
- Move hero image from lazy to priority loading
- Preconnect to image CDN origin
- Convert from PNG to AVIF/WebP
CLS Fix:
- Add width/height to all product images
- Reserve space for price badge that loads async
- Set font-display: optional on custom font
INP Fix:
- Wrap filter state update in startTransition
- Virtualize product grid (only render visible items)
- Move sort computation to Web Worker
Result: LCP = 1.8s, CLS = 0.02, INP = 120ms