Most performance advice online is generic. Use useMemo, lazy load images, code split your bundles. True, but it doesn't tell you where to actually spend your time on a real project.
Here are the patterns I reach for first, in the order they actually move the needle.
1. next/image Is Not Optional
This is the single highest leverage change on most sites, and people still skip it.
import Image from "next/image";
export function ProjectCard({ project }) {
return (
<Image
src={project.thumbnail}
alt={project.title}
width={600}
height={400}
className="rounded-lg"
priority={project.featured}
/>
);
}
next/image handles resizing, format conversion (serving WebP or AVIF where supported), and lazy loading automatically. Set priority only on the images visible above the fold, since that tells Next.js to preload them instead of lazy loading, which matters for Largest Contentful Paint.
2. Dynamic Imports for Anything Not Needed Immediately
A chart library, a rich text editor, a modal that only opens on click. None of that needs to be in the initial bundle.
import dynamic from "next/dynamic";
const RevenueChart = dynamic(() => import("@/components/RevenueChart"), {
loading: () => <ChartSkeleton />,
ssr: false,
});
ssr: false is worth it for anything that depends on browser only APIs (canvas, window, certain chart libraries). It keeps that code out of the server bundle entirely and only loads it once the client actually needs it.
3. Server Components by Default, Client Components as the Exception
Every 'use client' directive adds JavaScript that has to ship to the browser. The App Router defaults to Server Components, and staying there as long as possible is one of the biggest free performance wins Next.js gives you.
// ❌ The whole page becomes client JS for one button
"use client";
export default function ProductPage({ product }) {
const [inCart, setInCart] = useState(false);
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
<button onClick={() => setInCart(true)}>Add to cart</button>
</div>
);
}
// ✅ Only the interactive piece ships as client JS
export default function ProductPage({ product }) {
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
<AddToCartButton productId={product.id} />
</div>
);
}
// components/AddToCartButton.tsx
("use client");
export function AddToCartButton({ productId }) {
const [inCart, setInCart] = useState(false);
return <button onClick={() => setInCart(true)}>Add to cart</button>;
}
Same functionality, far less client JavaScript. The product name and description never needed to be interactive in the first place.
4. useMemo and useCallback Only Where They Actually Matter
These get overused. Wrapping every value in useMemo adds complexity without measurable benefit in most components. They're worth reaching for in two specific cases.
// Expensive computation that shouldn't rerun on every render
const sortedPosts = useMemo(() => {
return posts.sort((a, b) => b.date - a.date);
}, [posts]);
// A callback passed to a memoized child, to avoid breaking its memoization
const handleDelete = useCallback((id: string) => {
setItems((prev) => prev.filter((item) => item.id !== id));
}, []);
If a component renders fast and cheaply already, memoizing it adds overhead for no real gain. Reach for it when profiling actually shows a slow render, not by default on every function.
5. Font Optimization with next/font
Web fonts loaded the naive way cause layout shift and block rendering. next/font fixes both automatically.
// app/layout.tsx
import { Inter } from "next/font/google";
const inter = Inter({
subsets: ["latin"],
display: "swap",
});
export default function RootLayout({ children }) {
return (
<html lang="en" className={inter.className}>
<body>{children}</body>
</html>
);
}
Fonts get self-hosted at build time instead of fetched from Google's servers at request time, and display: swap prevents invisible text while the font loads.
6. Bundle Analysis Before Guessing
Don't optimize blind. Run an actual analysis and see what's taking up space.
npm install @next/bundle-analyzer
// next.config.ts
import withBundleAnalyzer from "@next/bundle-analyzer";
const bundleAnalyzer = withBundleAnalyzer({
enabled: process.env.ANALYZE === "true",
});
export default bundleAnalyzer({
// your existing config
});
ANALYZE=true npm run build
This opens a visual breakdown of what's actually in your JavaScript bundles. Nine times out of ten, one unexpected dependency (a full date library imported for one function, an icon set imported in full instead of per icon) turns out to be the biggest offender.
7. Avoid Prop Drilling That Forces Wide Re-renders
Passing state down through five layers of components to reach one that needs it causes every layer in between to re-render on change, even the ones that don't use the value.
// ❌ Every layer re-renders when theme changes
function App() {
const [theme, setTheme] = useState("dark");
return <Layout theme={theme} setTheme={setTheme} />;
}
// ✅ Only components that actually read the context re-render
const ThemeContext = createContext(null);
function App() {
const [theme, setTheme] = useState("dark");
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Layout />
</ThemeContext.Provider>
);
}
Context isn't free either, since anything consuming it re-renders when the value changes. But it's usually better than five components each re-rendering just to forward a prop they never use themselves.
Summary
| Pattern | Fixes |
| -------------------------------- | ---------------------------------------------------------- |
| next/image | Unoptimized images, layout shift, slow LCP |
| Dynamic imports | Heavy libraries loaded before they're needed |
| Server Components by default | Unnecessary client JavaScript shipped to the browser |
| Targeted useMemo/useCallback | Expensive recomputation, broken child memoization |
| next/font | Layout shift and render blocking from web fonts |
| Bundle analyzer | Guessing instead of measuring what's actually heavy |
| Context over deep prop drilling | Wide re-renders from state passed through unrelated layers |
The order above is roughly the order I'd actually work through on a real project. Images and Server Components alone usually account for most of the improvement before any of the finer grained tuning is even worth touching.
I apply this exact checklist across the dashboards and templates I build.
Get the templates: https://pixelanas.gumroad.com
Anas, full-stack Next.js developer building SaaS products and premium templates. X: @ASheikh69751