| name | performance |
| description | Core Web Vitals optimization and page speed for Next.js. This skill should be used when the user asks about "Core Web Vitals", "page speed", "PageSpeed", "LCP", "INP", "CLS", "Lighthouse score", "performance optimization", "next/image optimization", "next/font", "lazy loading", "code splitting", "bundle size", or needs to improve their site's loading speed and interaction responsiveness. |
Performance & Core Web Vitals
Core Web Vitals are Google's ranking signals measuring real user experience. Optimize for these three metrics.
Current Thresholds (2026)
| Metric | Good | Needs Improvement | Poor | What It Measures |
|---|
| LCP | < 2.5s | 2.5s - 4.0s | > 4.0s | Largest visible content painted |
| INP | < 200ms | 200ms - 500ms | > 500ms | Responsiveness to all interactions |
| CLS | < 0.1 | 0.1 - 0.25 | > 0.25 | Visual stability (layout shifts) |
INP replaced FID in March 2024. INP measures ALL interactions (clicks, taps, keyboard), not just the first one.
LCP Optimization (Largest Contentful Paint)
LCP measures how quickly the largest above-the-fold element renders.
Preload LCP Images
import Image from 'next/image'
<Image
src="/hero.jpg"
alt="Hero banner"
width={1200}
height={630}
priority
sizes="100vw"
/>
Only one image per page should have priority — the LCP element.
Optimize Image Formats
Configure AVIF (best compression) with WebP fallback:
const nextConfig = {
images: {
formats: ['image/avif', 'image/webp'],
remotePatterns: [
{ protocol: 'https', hostname: 'your-cms.com', pathname: '/assets/**' },
],
},
}
Responsive Images with sizes
Always provide sizes — without it, the browser downloads the largest image:
<Image src="/hero.jpg" alt="Hero" width={1920} height={1080} priority
sizes="100vw" />
<Image src="/card.jpg" alt="Card" width={400} height={300}
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw" />
<Image src="/sidebar.jpg" alt="Sidebar" width={300} height={200}
sizes="(max-width: 768px) 100vw, 300px" />
Server-Side Render Above the Fold
Use Server Components for LCP content — no hydration delay:
export default async function Home() {
const hero = await getHeroContent()
return (
<section>
<h1>{hero.title}</h1>
<Image src={hero.image} alt={hero.title} priority sizes="100vw"
width={1200} height={630} />
</section>
)
}
INP Optimization (Interaction to Next Paint)
INP measures responsiveness across ALL user interactions. Three phases: input delay + processing time + presentation delay.
Break Long Tasks
Any task >50ms blocks the main thread. Split heavy work:
'use client'
import { startTransition } from 'react'
function handleSearch(query: string) {
startTransition(() => {
setFilteredResults(filterLargeDataset(query))
})
}
Code Split Heavy Components
'use client'
import dynamic from 'next/dynamic'
const Chart = dynamic(() => import('@/components/chart'), {
loading: () => <div className="h-64 animate-pulse bg-muted rounded" />,
ssr: false,
})
Debounce Event Handlers
'use client'
import { useDeferredValue, useState } from 'react'
function SearchInput() {
const [query, setQuery] = useState('')
const deferredQuery = useDeferredValue(query)
return (
<>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<SearchResults query={deferredQuery} />
</>
)
}
Reduce DOM Size
Target: < 1,500 nodes, depth < 32, < 60 children per parent. For long lists, use virtual scrolling:
'use client'
import { useVirtualizer } from '@tanstack/react-virtual'
function VirtualList({ items }: { items: string[] }) {
const parentRef = useRef<HTMLDivElement>(null)
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 50,
})
return (
<div ref={parentRef} style={{ height: '400px', overflow: 'auto' }}>
<div style={{ height: `${virtualizer.getTotalSize()}px`, position: 'relative' }}>
{virtualizer.getVirtualItems().map((virtualItem) => (
<div key={virtualItem.key} style={{
position: 'absolute', top: 0, left: 0, width: '100%',
`${}`,
`(${})`,
}}>
{items[virtualItem.index]}
))}
)
}
CLS Optimization (Cumulative Layout Shift)
CLS measures visual stability — elements jumping around as the page loads.
Always Set Dimensions on Images
<Image src="/photo.jpg" alt="Photo" width={800} height={600} />
<div className="relative aspect-video">
<Image src="/photo.jpg" alt="Photo" fill sizes="100vw" />
</div>
<img src="/photo.jpg" alt="Photo" />
Use next/font to Prevent Font Shift
import { Inter } from 'next/font/google'
const inter = Inter({
subsets: ['latin'],
display: 'swap',
variable: '--font-sans',
})
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className={`${inter.variable} antialiased`}>
<body>{children}</body>
</html>
)
}
Tailwind CSS integration:
@import 'tailwindcss';
@theme inline {
--font-sans: var(--font-sans);
}
next/font downloads fonts at build time and self-hosts them — zero external requests, zero FOUT (Flash of Unstyled Text).
Avoid Content Injection Above Fold
Never insert banners, cookie notices, or ads above existing content. Reserve space with CSS:
<div className="min-h-12">
<Suspense fallback={<div className="h-12" />}>
<DynamicBanner />
</Suspense>
</div>
Caching & ISR
Incremental Static Regeneration
Serve static pages, revalidate in the background:
export const revalidate = 3600
export default async function BlogPost({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params
const post = await getPost(slug)
return <article>{/* content */}</article>
}
On-Demand Revalidation
Revalidate when content changes (via webhook from CMS):
import { revalidatePath, revalidateTag } from 'next/cache'
export async function POST(request: Request) {
const { path, tag } = await request.json()
if (path) revalidatePath(path)
if (tag) revalidateTag(tag)
return Response.json({ revalidated: true })
}
Measurement Tools
| Tool | Data Type | Use For |
|---|
| PageSpeed Insights | Field + Lab | Quick check of any URL |
| Google Search Console | Field | Real user data over time |
| Lighthouse (DevTools) | Lab | Detailed development audit |
| Web Vitals Extension | Field | Real-time monitoring during dev |
next build --profile | Lab | Identify slow build/render paths |
Quick Wins Checklist
| Action | Metric Impact | Effort |
|---|
Add priority to LCP image | LCP -500ms+ | Low |
Add sizes to all images | LCP, bandwidth | Low |
Use next/font | CLS elimination | Low |
| Set image width/height | CLS elimination | Low |
dynamic() for heavy components | INP, bundle size | Medium |
| Enable AVIF format | LCP, bandwidth | Low |
Use startTransition for non-urgent updates | INP | Medium |
ISR with revalidate | TTFB | Medium |