All resources
PerformanceIntermediate2 min read

Web Performance Checklist

Core Web Vitals targets and the Next.js techniques that move them: images, fonts, bundles and caching.

Know the targets

Google evaluates the 75th percentile of real user visits. Aim for the “good” column on mobile, where devices and networks are slowest.

MetricMeasuresGoodPoor
LCP — Largest Contentful PaintLoading≤ 2.5 s> 4 s
INP — Interaction to Next PaintResponsiveness≤ 200 ms> 500 ms
CLS — Cumulative Layout ShiftVisual stability≤ 0.1> 0.25

Images

  • Use next/image: it serves modern formats, resizes per device and reserves space to prevent layout shift.
  • Mark the hero image with priority so it is not lazy-loaded — it is usually the LCP element.
  • Always pass width and height (or fill with a sized parent).
  • Use sizes for responsive images so phones do not download desktop widths.
TSX
<Image  src="/hero.png"  alt="Dashboard preview"  width={1200}  height={720}  sizes="(max-width: 768px) 100vw, 1200px"  priority/>

Fonts

next/font self-hosts Google Fonts at build time, removes the extra connection to Google and adjusts the fallback font metrics to minimize layout shift.

layout.tsxTSX
import { Inter } from "next/font/google";const inter = Inter({ subsets: ["latin"], display: "swap" });export default function RootLayout({ children }) {  return (    <html lang="en">      <body className={inter.className}>{children}</body>    </html>  );}

Ship less JavaScript

  • Keep components as Server Components by default; add "use client" only to interactive leaves.
  • Pass server-rendered content to client components as props or children instead of importing large data modules in the client.
  • Split heavy widgets with next/dynamic and analyze bundles with @next/bundle-analyzer.
  • Break long tasks (> 50 ms) so input stays responsive — this is what INP measures.

Caching

AssetCache-Control
Hashed static files (/_next/static)public, max-age=31536000, immutable
HTML of static pagess-maxage on the CDN + revalidation
Personalized API responsesprivate, no-store

For data that changes slowly, revalidate on an interval instead of fetching on every request, e.g. fetch(url, { next: { revalidate: 3600 } }).