Profile apps, optimize bundle size, and implement advanced caching and streaming.
@next/bundle-analyzer.npm install -D @next/bundle-analyzer
next.config.js to analyze bundles.const withBundleAnalyzer = require("@next/bundle-analyzer")({
enabled: process.env.ANALYZE === "true",
});
module.exports = withBundleAnalyzer({});
ANALYZE=true npm run build
ANALYZE=true npm run build to open an interactive bundle treemap in the browser.web-vitals library or a Lighthouse run, not bundle size alone.npx lighthouse https://your-app.com --view
import dynamic from "next/dynamic";
const HeavyComponent = dynamic(() => import("./HeavyComponent"));
export default function Page() {
return <HeavyComponent />;
}
const Editor = dynamic(() => import("./Editor"), {
loading: () => <p>Loading editor...</p>,
});
import Image from "next/image";
<Image
src="/hero.jpg"
alt="Hero"
loading="lazy"
width={1200}
height={600}
/>
optimizePackageImports nested under the top-level experimental key in next.config.js — it is still an experimental flag, so the wrapper is required for it to take effect.// next.config.js
module.exports = {
experimental: {
optimizePackageImports: ["lodash", "date-fns"],
},
};
// Bad: imports entire lodash
import _ from "lodash";
// Good: imports only what you need
import debounce from "lodash/debounce";
useReportWebVitals from next/web-vitals, or by enabling Vercel Speed Insights for real-user production data.// app/_components/web-vitals.tsx
"use client";
import { useReportWebVitals } from "next/web-vitals";
export function WebVitals() {
useReportWebVitals((metric) => {
console.log(metric); // send to your analytics endpoint instead
});
return null;
}
Enable Partial Prerendering (PPR) in Next.js 15 to serve a static shell instantly while streaming dynamic slots.
// next.config.js
module.exports = {
experimental: { ppr: true }
};
Wrap only the dynamic parts of a page in <Suspense> — the static shell is served from the CDN edge, dynamic content streams in.
// app/product/[id]/page.tsx
import { Suspense } from "react";
import { ProductDetails } from "./ProductDetails"; // static — pre-rendered
import { RecommendedProducts } from "./RecommendedProducts"; // dynamic
export default function ProductPage() {
return (
<div>
<ProductDetails /> {/* served immediately from CDN */}
<Suspense fallback={<p>Loading recommendations...</p>}>
<RecommendedProducts /> {/* streamed after static shell */}
</Suspense>
</div>
);
}
Mark a component as dynamic using connection() from next/server — this opts it out of static prerendering.
import { connection } from "next/server";
export async function Cart() {
await connection(); // forces this component to be dynamic
const cart = await fetchCart();
return <ul>{cart.map(item => <li key={item.id}>{item.name}</li>)}</ul>;
}
PPR is composable — you can have multiple <Suspense> slots per page, each streaming independently.
PPR reduces Time To First Byte (TTFB) for pages with mixed static and dynamic content compared to full SSR.
// next.config.js
module.exports = {
compress: true, // enabled by default
};// next.config.js
module.exports = {
reactStrictMode: true,
};npm run build// next.config.js
module.exports = {
compiler: { removeConsole: { exclude: ["error"] } }
};Use the <Image> component for automatic format conversion, resizing, and lazy loading.
import Image from "next/image";
<Image
src="/hero.jpg"
alt="Hero banner"
width={1200}
height={600}
priority // preload above-the-fold images; omit for below-fold
quality={80} // default is 75; lower for smaller files
sizes="(max-width: 768px) 100vw, 50vw" // tell browser which size to download
/>
Set priority on hero images to preload them — only use it for images visible without scrolling.
Use the sizes prop so the browser downloads the smallest image that fits the layout.
// Full-width on mobile, half-width on desktop
<Image src="/photo.jpg" alt="Photo" fill sizes="(max-width: 640px) 100vw, 50vw" />
Load Google Fonts with zero layout shift using next/font/google.
import { Inter } from "next/font/google";
const inter = Inter({
subsets: ["latin"],
display: "swap", // avoids invisible text during font load
variable: "--font-inter"
});
export default function RootLayout({ children }) {
return <html className={inter.variable}><body>{children}</body></html>;
}
Self-host a local font with next/font/local to eliminate the external Google Fonts network request.
import localFont from "next/font/local";
const myFont = localFont({ src: "./fonts/GeistVF.woff2", display: "swap" });
Run ANALYZE=true next build with @next/bundle-analyzer configured in next.config.js to get an interactive treemap of your client and server bundles. Look for large node_modules being included in client chunks — these are usually the easiest wins.
Use next/dynamic for components that aren't needed on initial render — modals, charts, below-the-fold sections, and anything heavy like rich text editors. Static imports are fine for everything rendered in the initial viewport, since Next.js already tree-shakes and splits by route automatically.
SSG generates pages at build time (no revalidation), ISR adds revalidate to regenerate pages in the background after a set interval, and SSR runs on every request with no built-in cache unless you add Cache-Control headers manually. For most content pages, ISR with a short revalidation window gives the best balance of freshness and performance.
Yes, Next.js enables gzip compression by default when running next start, but it's intentionally disabled when you deploy behind a reverse proxy like Nginx or a CDN — those should handle compression instead to avoid double-encoding. If you're on a platform like Vercel, Brotli compression is applied automatically at the edge.
Wrap slow data-fetching components in <Suspense> with a fallback — Next.js will stream the shell HTML immediately and flush each suspended section as it resolves. For page-level streaming, loading.tsx files in the App Router act as automatic Suspense boundaries for the entire route segment.