A perfect Lighthouse score means nothing if your users are bouncing. Performance optimization that moves business metrics requires focusing on what actually matters: Largest Contentful Paint, Interaction to Next Paint, and Cumulative Layout Shift.
Core Web Vitals in 2026
Google's Core Web Vitals have evolved:
- LCP (Largest Contentful Paint): Under 2.5s is good
- INP (Interaction to Next Paint): Under 200ms is good
- CLS (Cumulative Layout Shift): Under 0.1 is good
// Measure Core Web Vitals in your app
import { onLCP, onINP, onCLS } from 'web-vitals'
function sendToAnalytics(metric: Metric) {
fetch('/api/analytics', {
method: 'POST',
body: JSON.stringify({
name: metric.name,
value: metric.value,
id: metric.id,
page: window.location.pathname
})
})
}
onLCP(sendToAnalytics)
onINP(sendToAnalytics)
onCLS(sendToAnalytics)Image Optimization Beyond next/image
// Priority loading for LCP images
<Image
src="/hero.jpg"
alt="Hero"
priority // Preloads immediately
sizes="(max-width: 768px) 100vw, 50vw" // Correct sizing
quality={85} // Balance quality vs size
placeholder="blur" // Prevents CLS
blurDataURL={heroBlurDataUrl}
/>
// Generate blur placeholders at build time
import { getPlaiceholder } from 'plaiceholder'
export async function getStaticProps() {
const { base64 } = await getPlaiceholder('/hero.jpg')
return { props: { blurDataURL: base64 } }
}JavaScript Bundle Analysis
# Analyze your bundle
npx @next/bundle-analyzer
# Or with the built-in stats
next build --debugCommon wins:
- Dynamic imports for heavy components
- Replace moment.js with date-fns or dayjs
- Use barrel file optimizations
- Remove unused dependencies
// ❌ Imports everything
import { format } from 'date-fns'
// ✅ Tree-shakeable
import format from 'date-fns/format'
// Dynamic import for heavy components
const HeavyEditor = dynamic(() => import('@/components/Editor'), {
loading: () => <EditorSkeleton />,
ssr: false // Skip SSR for client-only components
})Streaming and Suspense
// Improve perceived performance with streaming
import { Suspense } from 'react'
export default async function ProductPage({ params }) {
return (
<>
{/* Critical content renders immediately */}
<ProductHeader productId={params.id} />
{/* Non-critical content streams in */}
<Suspense fallback={<ReviewsSkeleton />}>
<ProductReviews productId={params.id} />
</Suspense>
<Suspense fallback={<RecommendationsSkeleton />}>
<Recommendations productId={params.id} />
</Suspense>
</>
)
}Real User Monitoring
// lib/monitoring.ts
import { onLCP, onINP, onCLS, onFCP, onTTFB } from 'web-vitals'
export function initRUM() {
const metrics: Record<string, number> = {}
function report(metric) {
metrics[metric.name] = metric.value
// Report after page is stable
if (Object.keys(metrics).length === 5) {
sendToMonitoring({
url: window.location.href,
userAgent: navigator.userAgent,
connection: navigator.connection?.effectiveType,
...metrics
})
}
}
onFCP(report)
onTTFB(report)
onLCP(report)
onINP(report)
onCLS(report)
}Quick Wins Checklist
□ Priority hints on LCP images
□ Font subsetting and display:swap
□ Preconnect to critical origins
□ Dynamic imports for heavy components
□ Placeholder images to prevent CLS
□ Debounced event handlers for INP
□ Size hints on images and embeds
□ Route prefetching for navigationConclusion
Performance optimization is iterative. Set up Real User Monitoring, identify your worst pages, apply targeted fixes, and measure impact. Synthetic benchmarks are starting points—real user data drives decisions.
