17. Performance Optimization
Understanding Core Web Vitals
Core Web Vitals are a set of user‑centric metrics that quantify loading performance, visual stability, and interactivity. Improving these scores directly impacts user satisfaction, SEO rankings, and conversion rates.
Largest Contentful Paint (LCP)
LCP measures the time from page start until the largest visible element (usually an image, video, or block‑level text) is rendered. A good LCP is ≤ 2.5 seconds; values between 2.5 s and 4.0 s need improvement; above 4.0 s are poor.
Formula: LCP = time(largest visible element paint) - time(navigation start)
Cumulative Layout Shift (CLS)
CLS quantifies unexpected layout shifts during the page lifecycle. It sums the impact fraction × distance fraction for each shift. A CLS < 0.1 is considered good.
Formula: CLS = Σ (impact fraction × distance fraction)
Interaction to Next Paint (INP)
INP captures the latency of all user interactions (clicks, taps, key presses) and reports the worst observed value. A good INP is ≤ 200 ms.
Formula: INP = max(latency of each interaction)
Image Optimization
Images often dominate LCP. Next.js provides the next/image component to automatically serve responsive, lazily‑loaded images in modern formats.
Basic Usage
import Image from "next/image";
<Image
src="/hero.jpg"
alt="Hero banner"
width={1200}
height={630}
priority
/>
The width and height attributes let the browser reserve space, preventing CLS. Setting priority marks the image as critical for LCP.
Remote Images
For external sources, whitelist domains in next.config.js:
// next.config.js
module.exports = {
images: {
domains: ["images.unsplash.com", "cdn.example.com"]
}
};
Responsive Breakpoints
next/image generates multiple sizes using the sizes attribute or default breakpoints, delivering WebP/AVIF when supported.
Font Optimization
External font requests can delay rendering and cause layout shifts. Self‑hosting or using next/font eliminates these issues.
Using next/font with Google Fonts
import { Inter } from "next/font/google";
const inter = Inter({ subsets: ["latin"], variable: "--font-inter" });
export default function RootLayout({ children }) {
return (
{children}
);
}
The generated CSS variable is applied to the root, ensuring fonts are available before first paint, reducing both LCP and CLS.
Lazy Loading
Deferring off‑screen resources reduces initial payload and improves LCP.
Image Lazy Loading (default)
next/image lazy‑loads images that are outside the viewport by default.
Component‑Level Lazy Loading
Use dynamic() with a loading placeholder:
import dynamic from "next/dynamic";
import Spinner from "@/components/Spinner";
const HeavyChart = dynamic(() => import("@/components/HeavyChart"), {
loading: () => ,
});
This splits the chart’s code into a separate bundle, fetched only when the component enters the viewport.
Dynamic Imports and Code Splitting
Breaking JavaScript into smaller chunks lowers the initial bundle size, directly benefiting INP by reducing main‑thread work.
Route‑Level Splitting (App Router)
Exporting a dynamic constant forces SSR or enables lazy loading:
export const dynamic = "force-dynamic"; // forces SSR, can be combined with lazy
Component‑Level Splitting
const Modal = dynamic(() => import("@/components/Modal"), {
ssr: false, // client‑only component
});
Marking a component as ssr: false prevents it from being rendered on the server, reducing HTML size and hydration time.
Additional Performance Tactics
- Compression: Enable
next compress(gzip/Brotli) to shrink text assets. - HTTP/2 Server Push: Push critical CSS/JS files via
Linkheaders. - Cache‑Control Headers: Set long‑lived
max‑agefor immutable assets andstale‑while‑revalidatefor frequently updated resources. - Monitoring: Use the
web-vitalslibrary or Vercel Analytics to capture real‑user metrics:
import { getCLS, getINP, getLCP } from 'web-vitals';
getCLS(console.log);
getINP(console.log);
getLCP(console.log);
Putting It All Together: Optimization Checklist
- Audit LCP with Chrome DevTools → identify the largest element.
- Replace unoptimized
<img>tags withnext/image, addingwidth,height, andprioritywhere appropriate. - Self‑host fonts or adopt
next/fontto eliminate external requests. - Apply
loading="lazy"to off‑screen images or usedynamic()for heavy components. - Split routes and components via dynamic imports to keep the initial JS bundle under 100 KB gzipped.
- Enable compression (gzip/Brotli) and configure HTTP/2 push for critical assets.
- Set proper
Cache‑Controlheaders:public, max‑age=31536000, immutablefor versioned files. - Monitor Core Web Vitals in production using
web-vitalsor Vercel Analytics and set performance budgets.
Example: Before & After Optimization
| Metric | Before | After | Target |
|---|---|---|---|
| LCP | 4.8 s | 2.1 s | ≤ 2.5 s (good) |
| CLS | 0.25 | 0.03 | < 0.1 (good) |
| INP | 320 ms | 140 ms | ≤ 200 ms (good) |
“Performance is not a feature; it’s the foundation of user experience.” – Web Performance Experts
Conclusion
By mastering image and font optimization, leveraging lazy loading and dynamic imports, applying compression and caching strategies, and continuously measuring Core Web Vitals, you can deliver Next.js applications that load fast, stay visually stable, and respond instantly to user interactions. Implement the checklist above, monitor the results, and iterate to keep performance budgets in check.