Performance optimization in Next.js 16 is not just about making pages load faster. It is about sending less JavaScript to the browser, loading resources at the right time, and using the server whenever possible.
Images are often one of the largest resources on a webpage. Instead of using a regular <img>, use next/image:
import Image from "next/image"<Imagesrc="/plants/monstera.jpg"alt="Monstera"fillwidth={800}height={800}/>
For responsive layouts, fill and sizes are especially useful. This allows Next.js and the browser to select an appropriate image resource instead of downloading unnecessarily large images.
Loading fonts directly from external providers can add extra network requests. Next.js provides next/font to optimize and self-host fonts.
import { Inter } from "next/font/google"const inter = Inter({subsets: ["latin"],weight: ["400", "500", "600", "700"],})
Then:
<html className={inter.className}>
Only load the font weights you actually use. If your application uses four weights, there is no reason to load nine.
next/link for Internal NavigationFor internal routes, use Link instead of regular anchor navigation:
import Link from "next/link"<Link href="/plants">Plants</Link>
next/link enables Next.js navigation behavior and can prefetch routes when appropriate. This makes navigation feel faster without requiring a full document reload.
For external websites, continue using a normal <a> element.
Analytics, tracking tools, chat widgets, and other third-party scripts can add significant JavaScript to your application.
Use next/script instead of manually inserting scripts:
import Script from "next/script"<Scriptsrc="https://example.com/analytics.js"strategy="afterInteractive"/>
Choose the loading strategy based on how important the script is. Critical scripts may need early loading, while analytics and widgets can usually be loaded later.
The goal is simple: don’t let third-party JavaScript block the critical rendering path.
One of the biggest performance advantages of the App Router is React Server Components.
If a component doesn’t need browser interaction, keep it on the server:
export default async function Products() {const products = await getProducts()return (<div>{products.map((product) => (<div key={product.id}>{product.name}</div>))}</div>)}
Avoid adding:
"use client"
unless the component actually needs client-side features such as useState, event handlers, browser APIs, or other client-only functionality.
Some components don’t need to be included in the initial JavaScript bundle.
For example, a large chart or editor can be loaded dynamically:
import dynamic from "next/dynamic"const Chart = dynamic(() => import("./Chart"))
This allows heavy code to be loaded only when it is needed.
A useful rule is:
If a feature is expensive and not required for the initial page, consider loading it dynamically.
Next.js App Router provides special files for async UI states:
app/└── plants/├── page.tsx├── loading.tsx└── error.tsx
For example:
export default function Loading() {return <div>Loading plants...</div>}
This gives users immediate feedback instead of leaving them with a blank screen while content is loading.
Not every page needs to fetch fresh data on every request. For content such as blogs, product catalogs, or other data that can tolerate some caching, ISR can be useful:
export const revalidate = 3600
The page can be cached and regenerated periodically instead of performing the same expensive work for every request.
Think of ISR as:
Request↓Cached content↓Revalidation↓Fresh content
This can reduce server work and improve response times.