|$ curl https://forge-ai.dev/api/markdown?path=docs/nextjs/optimization
$cat docs/next.js-optimization.md
updated Recentlyยท40 min readยทpublished

Next.js Optimization

โ—†Next.jsโ—†Performanceโ—†Optimizationโ—†Intermediate to Advanced๐ŸŽฏFree Tools
Why Optimize?

Next.js provides many built-in optimizations โ€” automatic code splitting, image optimization, font loading, and caching. But there are additional strategies and configurations that can significantly improve your application's performance. This guide covers the most impactful optimizations for Core Web Vitals and user experience.

โ„น

info

Next.js optimizes many things by default. Before adding manual optimizations, measure your current performance with Lighthouse, Web Vitals, or Vercel Analytics to identify the specific areas that need improvement.
Image Optimization

The next/image component optimizes images automatically โ€” lazy loading, responsive sizing, format conversion (WebP/AVIF), and preventing layout shift. It is one of the highest-impact optimizations you can make.

components/optimized-images.tsx
TSX
1import Image from "next/image";
2
3// Basic usage with automatic optimization
4export function HeroImage() {
5 return (
6 <Image
7 src="/hero.jpg"
8 alt="Hero banner"
9 width={1200}
10 height={600}
11 priority // Load immediately (above the fold)
12 />
13 );
14}
15
16// Responsive image with srcSet
17export function ResponsiveImage() {
18 return (
19 <Image
20 src="/photo.jpg"
21 alt="A beautiful landscape"
22 fill // Fill parent container
23 sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
24 className="object-cover"
25 />
26 );
27}
28
29// Remote image
30export function Avatar({ src, name }: { src: string; name: string }) {
31 return (
32 <Image
33 src={src}
34 alt={name}
35 width={48}
36 height={48}
37 className="rounded-full"
38 />
39 );
40}
PropPurposeWhen to Use
priorityPreload image (skip lazy loading)Above-the-fold images, hero banners
fillFill parent containerBackground images, cards with unknown size
sizesResponsive size hintsAlways set with fill or responsive images
placeholderBlur-up or empty placeholderImages that take time to load
qualityImage quality (1-100)Default 75 is usually optimal
image-config.tsx
TSX
1// Image with blur placeholder
2import Image from "next/image";
3import { blurDataURL } from "@/lib/blur";
4
5export function GalleryImage() {
6 return (
7 <Image
8 src="/gallery/photo.jpg"
9 alt="Gallery photo"
10 width={400}
11 height={300}
12 placeholder="blur"
13 blurDataURL={blurDataURL} // Small base64 encoded blur image
14 />
15 );
16}
17
18// Configure remote image domains in next.config.ts
19// next.config.ts
20const nextConfig: NextConfig = {
21 images: {
22 remotePatterns: [
23 { protocol: "https", hostname: "images.unsplash.com" },
24 { protocol: "https", hostname: "**.amazonaws.com" },
25 ],
26 formats: ["image/avif", "image/webp"],
27 },
28};
๐Ÿ”ฅ

pro tip

Always set width and height (or fill) on the Image component. Without dimensions, the image causes layout shift (CLS). Use priority for the largest above-the-fold image to eliminate LCP delays.
Font Optimization

next/font optimizes font loading by downloading fonts at build time, self-hosting them, and providing automaticsize-adjust to prevent Cumulative Layout Shift (CLS).

app/layout.tsx
TSX
1// app/layout.tsx
2import { Inter, JetBrains_Mono } from "next/font/google";
3
4const inter = Inter({
5 subsets: ["latin"],
6 display: "swap", // Use swap to prevent FOIT
7 variable: "--font-inter", // CSS variable
8});
9
10const jetbrainsMono = JetBrains_Mono({
11 subsets: ["latin"],
12 display: "swap",
13 variable: "--font-mono",
14});
15
16export default function RootLayout({
17 children,
18}: {
19 children: React.ReactNode;
20}) {
21 return (
22 <html lang="en" className={`${inter.variable} ${jetbrainsMono.variable}`}>
23 <body className="font-sans">
24 {children}
25 </body>
26 </html>
27 );
28}
local-fonts.tsx
TSX
1// Local fonts
2import localFont from "next/font/local";
3
4const myFont = localFont({
5 src: [
6 { path: "./fonts/Inter-Regular.woff2", weight: "400", style: "normal" },
7 { path: "./fonts/Inter-Bold.woff2", weight: "700", style: "normal" },
8 { path: "./fonts/Inter-Italic.woff2", weight: "400", style: "italic" },
9 ],
10 display: "swap",
11 variable: "--font-inter",
12});
13
14// Use in layout
15export default function Layout({ children }) {
16 return (
17 <html className={myFont.variable}>
18 <body className="font-sans">{children}</body>
19 </html>
20 );
21}
โ„น

info

next/font self-hosts Google Fonts at build time, eliminating the third-party connection to fonts.googleapis.com. This improves privacy, reliability, and performance. The font files are served from the same origin with immutable cache headers.
Metadata API

The Metadata API generates optimized <head> tags for SEO, social sharing, and browser behavior. Use the static metadata export or the dynamic generateMetadata function.

app/layout.tsx
TSX
1// Static metadata in layout.tsx (applies to all pages)
2import type { Metadata, Viewport } from "next";
3
4export const metadata: Metadata = {
5 title: {
6 template: "%s | MyApp",
7 default: "MyApp โ€” Build Something Amazing",
8 },
9 description: "A comprehensive platform for developers",
10 keywords: ["next.js", "react", "web development"],
11 authors: [{ name: "Your Name" }],
12 creator: "Your Name",
13 openGraph: {
14 type: "website",
15 locale: "en_US",
16 url: "https://myapp.com",
17 siteName: "MyApp",
18 title: "MyApp โ€” Build Something Amazing",
19 description: "A comprehensive platform for developers",
20 images: [
21 { url: "/og-default.png", width: 1200, height: 630, alt: "MyApp" },
22 ],
23 },
24 twitter: {
25 card: "summary_large_image",
26 title: "MyApp",
27 description: "A comprehensive platform for developers",
28 images: ["/og-default.png"],
29 },
30 robots: {
31 index: true,
32 follow: true,
33 googleBot: {
34 index: true,
35 follow: true,
36 "max-video-preview": -1,
37 "max-image-preview": "large",
38 "max-snippet": -1,
39 },
40 },
41 icons: {
42 icon: "/favicon.ico",
43 shortcut: "/favicon-16x16.png",
44 apple: "/apple-touch-icon.png",
45 },
46};
47
48export const viewport: Viewport = {
49 width: "device-width",
50 initialScale: 1,
51 maximumScale: 5,
52 themeColor: [
53 { media: "(prefers-color-scheme: dark)", color: "#000000" },
54 { media: "(prefers-color-scheme: light)", color: "#ffffff" },
55 ],
56};
app/blog/[slug]/page.tsx
TSX
1// Dynamic metadata for individual pages
2import type { Metadata } from "next";
3
4export async function generateMetadata({
5 params,
6}: {
7 params: Promise<{ slug: string }>;
8}): Promise<Metadata> {
9 const { slug } = await params;
10 const post = await getPost(slug);
11
12 if (!post) {
13 return { title: "Post Not Found" };
14 }
15
16 return {
17 title: post.title,
18 description: post.excerpt,
19 openGraph: {
20 title: post.title,
21 description: post.excerpt,
22 type: "article",
23 publishedTime: post.publishedAt.toISOString(),
24 authors: [post.author.name],
25 tags: post.tags,
26 images: [post.coverImage],
27 },
28 };
29}
Sitemap Generation

Generate sitemaps dynamically using the sitemap.ts file convention. This creates asitemap.xml at the root of your site.

app/sitemap.ts
TSX
1// app/sitemap.ts
2import type { MetadataRoute } from "next";
3
4export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
5 const baseUrl = "https://myapp.com";
6
7 // Static pages
8 const staticPages = [
9 { url: baseUrl, lastModified: new Date(), changeFrequency: "daily" as const, priority: 1.0 },
10 { url: `${baseUrl}/about`, lastModified: new Date(), changeFrequency: "monthly" as const, priority: 0.8 },
11 { url: `${baseUrl}/pricing`, lastModified: new Date(), changeFrequency: "monthly" as const, priority: 0.8 },
12 ];
13
14 // Dynamic pages from database
15 const posts = await db.post.findMany({
16 select: { slug: true, updatedAt: true },
17 where: { published: true },
18 });
19
20 const blogPages = posts.map(post => ({
21 url: `${baseUrl}/blog/${post.slug}`,
22 lastModified: post.updatedAt,
23 changeFrequency: "weekly" as const,
24 priority: 0.6,
25 }));
26
27 // Dynamic pages from API
28 const products = await db.product.findMany({
29 select: { id: true, updatedAt: true },
30 });
31
32 const productPages = products.map(product => ({
33 url: `${baseUrl}/products/${product.id}`,
34 lastModified: product.updatedAt,
35 changeFrequency: "daily" as const,
36 priority: 0.7,
37 }));
38
39 return [...staticPages, ...blogPages, ...productPages];
40}

For Next.js 15+, use the route segment config:

app/sitemap.xml/route.ts
TSX
1// Alternative: app/sitemap.xml/route.ts
2import { db } from "@/lib/database";
3
4export async function GET() {
5 const baseUrl = "https://myapp.com";
6
7 const posts = await db.post.findMany({
8 select: { slug: true, updatedAt: true },
9 where: { published: true },
10 });
11
12 const urls = posts
13 .map(
14 (post) => ` <url>
15 <loc>${baseUrl}/blog/${post.slug}</loc>
16 <lastmod>${post.updatedAt.toISOString()}</lastmod>
17 <changefreq>weekly</changefreq>
18 <priority>0.6</priority>
19 )
20 .join("\n");
21
22 const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
23<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
24 <url>
25 <loc>${baseUrl}</loc>
26 <changefreq>daily</changefreq>
27 <priority>1.0</priority>
28 </url>
29${urls}
30</urlset>`;
31
32 return new Response(sitemap, {
33 headers: { "Content-Type": "application/xml" },
34 });
35}
Robots.txt

Generate robots.txt dynamically with the robots.ts file convention.

app/robots.ts
TSX
1// app/robots.ts
2import type { MetadataRoute } from "next";
3
4export default function robots(): MetadataRoute.Robots {
5 return {
6 rules: [
7 {
8 userAgent: "*",
9 allow: "/",
10 disallow: ["/admin", "/api", "/dashboard", "/_next/"],
11 },
12 {
13 userAgent: "GPTBot",
14 disallow: "/",
15 },
16 ],
17 sitemap: "https://myapp.com/sitemap.xml",
18 };
19}
Code Splitting & Lazy Loading

Next.js automatically code-splits at the route level. Each page only loads the JavaScript needed for that route. You can further optimize by dynamically importing heavy components.

components/analytics-section.tsx
TSX
1// Dynamic import โ€” component is code-split into a separate chunk
2import dynamic from "next/dynamic";
3
4// Load a heavy chart component only when needed
5const HeavyChart = dynamic(() => import("@/components/heavy-chart"), {
6 loading: () => (
7 <div className="h-96 bg-gray-800 animate-pulse rounded-lg" />
8 ),
9 ssr: false, // Skip SSR for purely client-side components
10});
11
12// Load a modal only when it is opened
13const VideoPlayer = dynamic(() => import("@/components/video-player"), {
14 loading: () => <div className="h-64 bg-gray-800 rounded-lg" />,
15});
16
17export function AnalyticsSection() {
18 const [showChart, setShowChart] = useState(false);
19
20 return (
21 <div>
22 <button onClick={() => setShowChart(true)}>
23 Show Analytics
24 </button>
25 {showChart && <HeavyChart />}
26 </div>
27 );
28}
โ„น

info

Use dynamic() with ssr: false for components that depend on browser APIs or are heavy and not needed for initial render. This keeps the initial JavaScript bundle small.
Bundle Analysis

Analyze your bundle to find opportunities for optimization. Use @next/bundle-analyzer to visualize your JavaScript bundle composition.

terminal
Bash
1# Install bundle analyzer
2npm i -D @next/bundle-analyzer
3
4# next.config.ts
5import withBundleAnalyzer from "@next/bundle-analyzer";
6
7const withAnalyzer = withBundleAnalyzer({ enabled: true });
8export default withAnalyzer(nextConfig);
9
10# Build and analyze
11ANALYZE=true npm run build
next.config.ts
TypeScript
1// next.config.ts with bundle analyzer
2import type { NextConfig } from "next";
3import withBundleAnalyzer from "@next/bundle-analyzer";
4
5const nextConfig: NextConfig = {
6 // ... other config
7};
8
9const wrappedConfig = withBundleAnalyzer({
10 enabled: process.env.ANALYZE === "true",
11})(nextConfig);
12
13export default wrappedConfig;
Caching Headers

Set appropriate cache headers for static assets and API responses to maximize CDN cache hits.

next.config.ts
TypeScript
1// next.config.ts โ€” Cache headers for static assets
2const nextConfig: NextConfig = {
3 async headers() {
4 return [
5 {
6 // Cache immutable static assets for 1 year
7 source: "/_next/static/(.*)",
8 headers: [
9 {
10 key: "Cache-Control",
11 value: "public, max-age=31536000, immutable",
12 },
13 ],
14 },
15 {
16 // Cache images for 30 days
17 source: "/images/(.*)",
18 headers: [
19 {
20 key: "Cache-Control",
21 value: "public, max-age=2592000, stale-while-revalidate=86400",
22 },
23 ],
24 },
25 ];
26 },
27};
Edge Middleware for Performance

Use middleware at the edge for geo-based redirects, A/B testing, and authentication checks without hitting your origin server.

middleware.ts
TSX
1// middleware.ts โ€” Edge-optimized request handling
2import { NextResponse } from "next/server";
3import type { NextRequest } from "next/server";
4
5export function middleware(request: NextRequest) {
6 const response = NextResponse.next();
7
8 // A/B testing โ€” assign variant based on cookie
9 const variant = request.cookies.get("ab-variant")?.value;
10 if (!variant) {
11 const randomVariant = Math.random() > 0.5 ? "a" : "b";
12 response.cookies.set("ab-variant", randomVariant, {
13 maxAge: 60 * 60 * 24 * 30, // 30 days
14 path: "/",
15 });
16 }
17
18 // Geo-based redirect (if using edge runtime)
19 const country = request.geo?.country;
20 if (country === "DE" && !request.nextUrl.pathname.startsWith("/de")) {
21 return NextResponse.redirect(new URL(`/de${request.nextUrl.pathname}`, request.url));
22 }
23
24 // Security headers
25 response.headers.set("X-Request-Id", crypto.randomUUID());
26
27 return response;
28}
29
30export const config = {
31 matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
32};
Core Web Vitals Checklist

Key metrics and how to optimize them in Next.js:

LCP (Largest Contentful Paint)

Optimize with priority on the hero Image, self-hosted fonts with next/font, and streaming with Suspense. Target: under 2.5 seconds.

INP (Interaction to Next Paint)

Keep Client Components small and at the leaf of the tree. Use startTransition for non-urgent state updates. Target: under 200ms.

CLS (Cumulative Layout Shift)

Always set width/height on images, use next/font with display: "swap", and reserve space for dynamic content. Target: under 0.1.

โœ“

best practice

Run Lighthouse on every deployment to catch performance regressions early. Use Vercel Analytics or a similar tool to monitor real-user Core Web Vitals in production. Optimizations should be driven by data, not assumptions.
$Blueprint โ€” Engineering DocumentationยทSection ID: NEXTJS-08ยทRevision: 1.0