Performance5 min read
Image Optimization, Lazy Loading, and Code Splitting in Next.js
How to use next/image, native lazy loading, next/dynamic, and next/font in the Next.js App Router to load less and load it at the right time.
By Nitin Wagmode
Next.js includes several built-in tools for performance: an image component, automatic route-based code splitting, dynamic imports, and font optimization. Using them well can reduce how much a page downloads and improve metrics like LCP and CLS.
The examples below use the App Router. Details can differ between Next.js versions, so check the documentation for the version your project uses.
Images with next/image
The Image component from next/image builds on the native <img> element and adds:
- Resizing and modern formats: images are served at sizes suited to the device, and in formats such as WebP when the browser supports them.
- Lazy loading by default: images below the fold load as they approach the viewport.
- Layout stability: required dimensions mean the browser can reserve space, which helps avoid layout shift.
import Image from "next/image";
export function TeamPhoto() {
return (
<Image
src="/images/team.jpg"
alt="The team at the office"
width={1200}
height={800}
sizes="(min-width: 1024px) 50vw, 100vw"
/>
);
}Tell the browser how wide the image will be
The sizes prop describes how wide the image displays at different viewport sizes. Without it, the browser may download a larger file than needed. In the example above, the image takes half the viewport on large screens and the full width otherwise.
Prioritize the LCP image
Images are lazy-loaded by default, which is wrong for the main image at the top of the page. For the hero image that is likely to be the Largest Contentful Paint element, mark it as a priority so it loads eagerly with high priority:
<Image src="/hero.jpg" alt="Dashboard preview" width={1600} height={900} priority />Use this for one or two above-the-fold images, not every image. Newer Next.js versions may rename or replace this option, so confirm the current prop name in the docs for your version.
Images that fill a container
When you do not know the exact dimensions, fill makes the image cover its parent. The parent needs a position and a size:
<div style={{ position: "relative", aspectRatio: "16 / 9" }}>
<Image src={post.cover} alt={post.title} fill sizes="(min-width: 768px) 33vw, 100vw" style={{ objectFit: "cover" }} />
</div>Remote images
Images from another domain must be allowed in next.config.js, which prevents your image optimizer from being used for arbitrary URLs:
module.exports = {
images: {
remotePatterns: [{ protocol: "https", hostname: "images.example.com" }],
},
};Lazy loading other content
Native lazy loading also works for iframes, which is useful for embedded videos and maps:
<iframe src="https://www.youtube-nocookie.com/embed/VIDEO_ID" title="Product demo" loading="lazy" width="560" height="315"></iframe>For heavy embeds, a lighter option is a placeholder image with a play button that loads the real embed only when clicked.
Code splitting
Automatic route splitting
Next.js splits JavaScript by route automatically. Visiting /about does not download the code that only /dashboard uses. In the App Router, Server Components add another benefit: their code runs on the server and is not sent to the browser at all.
A practical way to reduce client JavaScript is to keep "use client" at the edges. Make small interactive components client components, and let the pages and layouts around them stay as Server Components.
Dynamic imports with next/dynamic
For large client-side components that are not needed immediately, such as a chart, rich text editor, or map, next/dynamic loads them separately:
"use client";
import { useState } from "react";
import dynamic from "next/dynamic";
const SalesChart = dynamic(() => import("./SalesChart"), {
loading: () => <p>Loading chart…</p>,
ssr: false, // only if the component depends on browser-only APIs
});
export function Reports() {
const [showChart, setShowChart] = useState(false);
return (
<>
<button type="button" onClick={() => setShowChart(true)}>Show chart</button>
{showChart && <SalesChart />}
</>
);
}The chart's code downloads only when the user asks for it. Use ssr: false only when a component truly cannot render on the server, because it removes that content from the initial HTML.
Importing libraries on demand
The same idea works for libraries used in a single interaction:
async function exportToCsv(rows) {
const { unparse } = await import("papaparse");
const csv = unparse(rows);
// download csv...
}Fonts with next/font
Web fonts can block rendering and cause layout shift. next/font downloads fonts at build time, serves them from your own domain, and generates fallback font metrics to reduce shifting when the font loads:
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>
);
}Loading only the subsets and weights you use keeps font files small.
Measuring the effect
Check whether a change helped rather than assuming it did:
- Run a production build (
next build), which prints the JavaScript size of each route. - Use
@next/bundle-analyzerto see which modules make bundles large. - Compare Lighthouse or WebPageTest runs before and after, using medians of several runs.
- Watch real-user Core Web Vitals after deploying.
Key takeaways
next/imagehandles resizing, modern formats, lazy loading, and reserved space; always provide a correctsizesvalue.- Prioritize the LCP image and do not lazy-load above-the-fold content.
- Keep client components small so more code stays on the server.
- Use
next/dynamicand dynamicimport()for heavy, non-critical code. - Use
next/fontto self-host fonts and reduce layout shift.
Conclusion
Next.js provides strong performance defaults, but they work best when you give them the right information: image sizes, which content is critical, and which code can wait. Measure before and after, and focus on changes that improve what users see first.
- #Next.js
- #Web Performance
- #Image Optimization
- #Code Splitting
Keep reading
- Oct 24, 2026
Understanding Core Web Vitals for Frontend Developers
What LCP, INP, and CLS measure, the thresholds Google uses, the difference between field and lab data, and the most common frontend causes of poor scores.5 min read - Oct 24, 2026
How to Improve Lighthouse Performance Scores
How the Lighthouse performance score is calculated, why it varies between runs, how to read the report, and practical fixes for the audits that matter most.5 min read - Sep 26, 2026
Moving an E-commerce Frontend to Next.js SSR
A step-by-step path for migrating a client-rendered React storefront to Next.js server-side rendering without stopping feature work.6 min read