Core Web Vitals directly impact user experience and search rankings. In 2026, with INP replacing FID, performance optimization is more important than ever. This guide covers practical techniques to achieve excellent performance scores.
Core Web Vitals Targets
| Metric | Good | Needs Improvement | Poor |
|--------|-----------|-------------------|----------|
| LCP | < 2.5s | 2.5s - 4s | > 4s |
| INP | < 200ms | 200ms - 500ms | > 500ms |
| CLS | < 0.1 | 0.1 - 0.25 | > 0.25 |
LCP (Largest Contentful Paint): Loading performance
INP (Interaction to Next Paint): Responsiveness
CLS (Cumulative Layout Shift): Visual stabilityOptimizing LCP
// Next.js image optimization for LCP
import Image from 'next/image';
// Hero image - LCP element
export function Hero() {
return (
<section className="relative h-[600px]">
<Image
src="/hero.webp"
alt="Hero image"
fill
priority // Preload for LCP
sizes="100vw"
className="object-cover"
placeholder="blur"
blurDataURL="data:image/jpeg;base64,..."
/>
<div className="relative z-10">
<h1 className="text-5xl font-bold">Welcome</h1>
</div>
</section>
);
}<!-- Preload critical resources -->
<head>
<!-- Preload LCP image -->
<link
rel="preload"
as="image"
href="/hero.webp"
fetchpriority="high"
/>
<!-- Preload critical fonts -->
<link
rel="preload"
as="font"
type="font/woff2"
href="/fonts/inter.woff2"
crossorigin
/>
<!-- Preconnect to external origins -->
<link rel="preconnect" href="https://api.example.com" />
<link rel="dns-prefetch" href="https://analytics.example.com" />
</head>Optimizing INP
// Optimize interactions for INP
import { useTransition, useDeferredValue, startTransition } from 'react';
// Use transitions for non-urgent updates
function SearchResults({ query }: { query: string }) {
const [isPending, startTransition] = useTransition();
const [results, setResults] = useState([]);
const handleSearch = (value: string) => {
// Urgent: update input immediately
setQuery(value);
// Non-urgent: defer results update
startTransition(() => {
const filtered = filterResults(value);
setResults(filtered);
});
};
return (
<div>
<input onChange={(e) => handleSearch(e.target.value)} />
{isPending && <Spinner />}
<ResultsList results={results} />
</div>
);
}
// Defer expensive computations
function ProductList({ products, filter }) {
// Defer filter computation
const deferredFilter = useDeferredValue(filter);
const filtered = useMemo(
() => products.filter(p => matchesFilter(p, deferredFilter)),
[products, deferredFilter]
);
return <List items={filtered} />;
}
// Break up long tasks
async function processLargeDataset(items: Item[]) {
const CHUNK_SIZE = 100;
const results = [];
for (let i = 0; i < items.length; i += CHUNK_SIZE) {
const chunk = items.slice(i, i + CHUNK_SIZE);
const processed = processChunk(chunk);
results.push(...processed);
// Yield to main thread between chunks
await new Promise(resolve => setTimeout(resolve, 0));
}
return results;
}
// Use Web Workers for heavy computation
const worker = new Worker(
new URL('./worker.ts', import.meta.url)
);
worker.postMessage({ data: largeDataset });
worker.onmessage = (e) => {
setResults(e.data);
};Optimizing CLS
// Prevent layout shifts
// Always specify dimensions for images
<Image
src="/product.jpg"
alt="Product"
width={400}
height={300} // Explicit dimensions prevent shift
/>
// Reserve space for dynamic content
function AdBanner() {
const [loaded, setLoaded] = useState(false);
return (
<div
className="min-h-[250px]" // Reserve space
style={{ aspectRatio: '728/90' }}
>
{loaded ? <Ad /> : <AdSkeleton />}
</div>
);
}
// Font loading without shift
<style>
@font-face {
font-family: 'Inter';
src: url('/fonts/inter.woff2') format('woff2');
font-display: swap; /* or 'optional' for less shift */
size-adjust: 100%; /* Match fallback size */
}
</style>
// Avoid inserting content above existing content
function Notifications() {
// Bad: Prepending shifts content down
// notifications.unshift(newNotification);
// Good: Append to bottom or use fixed position
notifications.push(newNotification);
// Or use transform instead of layout changes
return (
<div className="fixed top-4 right-4">
{notifications.map(n => (
<Toast key={n.id} notification={n} />
))}
</div>
);
}Bundle Optimization
// Code splitting strategies
import dynamic from 'next/dynamic';
// Lazy load heavy components
const Chart = dynamic(() => import('./Chart'), {
loading: () => <ChartSkeleton />,
ssr: false, // Client-only component
});
const MarkdownEditor = dynamic(
() => import('./MarkdownEditor'),
{ loading: () => <EditorSkeleton /> }
);
// Route-based splitting (automatic in Next.js App Router)
// Each page is automatically code-split
// Lazy load below-fold content
function ProductPage() {
return (
<>
<ProductHero /> {/* Critical - loaded immediately */}
<Suspense fallback={<ReviewsSkeleton />}>
<ProductReviews /> {/* Lazy loaded */}
</Suspense>
<Suspense fallback={<RelatedSkeleton />}>
<RelatedProducts /> {/* Lazy loaded */}
</Suspense>
</>
);
}
// Tree-shake imports
// Bad: imports entire library
import _ from 'lodash';
_.debounce(fn, 300);
// Good: import only what you need
import debounce from 'lodash/debounce';
debounce(fn, 300);
// Even better: use native or smaller alternatives
function debounce(fn, ms) {
let timeoutId;
return (...args) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn(...args), ms);
};
}Measuring Performance
// Real User Monitoring (RUM)
import { onLCP, onINP, onCLS } from 'web-vitals';
function sendToAnalytics(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 });
}
}
onLCP(sendToAnalytics);
onINP(sendToAnalytics);
onCLS(sendToAnalytics);
// Custom performance marks
performance.mark('hero-start');
await loadHeroImage();
performance.mark('hero-end');
performance.measure('hero-load-time', 'hero-start', 'hero-end');
const measure = performance.getEntriesByName('hero-load-time')[0];
console.log(`Hero loaded in ${measure.duration}ms`);Performance Checklist
Performance Optimization Checklist
Loading (LCP):
- Preload LCP image/resource
- Use next-gen formats (WebP, AVIF)
- Optimize server response time
- Use CDN for static assets
Interactivity (INP):
- Break up long tasks
- Use React transitions
- Defer non-critical JavaScript
- Use Web Workers for heavy computation
Visual Stability (CLS):
- Set explicit dimensions on images/videos
- Reserve space for dynamic content
- Avoid inserting content above existing content
- Use transform for animations
Conclusion
Web performance is a continuous effort that directly impacts user experience and business metrics. Focus on the Core Web Vitals, measure real user data, and iterate. The techniques in this guide will help you achieve excellent performance scores.
Need help optimizing your web performance? Contact Jishu Labs for expert frontend performance consulting.
About Emily Zhang
Emily Zhang is the Frontend Lead at Jishu Labs with deep expertise in web performance optimization.