Image Optimization in Next.js
This article was updated for Next.js 16.
For as long as I've worked on web performance, images have been the obvious place to start. They're usually the heaviest assets on any page, and optimizing them has the most visible impact. The principle is straightforward: serve the right format, at the right size, at the right time. But getting there has been a long journey.
In 2017, I contributed webp support to Gatsby's image component. Gatsby was the go-to React framework at the time, popular enough that even the official React documentation ran on it. WebP was the new format everyone was excited about, because it allowed for significantly smaller file sizes without quality loss. Since the format was new (and Internet Explorer was still around), browser support was inconsistent. You had to serve webp inside a <picture> element with a jpg or png fallback. That was state-of-the-art image optimization in a React context.
A lot has changed since then while the industry has been constantly improving on image performance. The AV1 Image File Format (AVIF) was released and typically offers 50% better compression than JPEG and even 20%-30% better than WebP. Both WebP and AVIF are now supported by all major browsers. Native lazy loading, responsive image selection, and priority hints ship out of the box in these browsers. Additionally, the next/image component handles most of the heavy lifting automatically.

Images are the heaviest assets
Images are typically the heaviest assets on any web page. Product listings, news article headers, hero sections, marketing landing pages - images dominate the payload. Unoptimized images are often the single biggest factor in slow page loads, and they directly affect your Largest Contentful Paint score. Slower pages mean higher bounce rates, lower conversions, and higher CDN costs from serving oversized files.
Browser primitives today
It's worth stepping back and looking at what browsers can do natively now, because next/image builds on top of these features:
srcsetandsizeslet the browser pick the right image resolution for the device. No JavaScript needed.loading="lazy"defers off-screen images until the user scrolls near them. Native, no library required, supported across all major browsers since 2020.fetchPriority="high"hints to the browser that a specific resource should be prioritized over others.- WebP and AVIF are supported by all major browsers. Both formats offer significantly smaller file sizes than jpg and png at comparable visual quality.
A good framework image component uses these primitives and adds what you'd otherwise have to build yourself: automatic format conversion, on-demand resizing, responsive srcset generation, and sensible defaults.
The next/image component
The next/image component wraps a standard <img> element and adds several optimizations:
- Format conversion: accepts any input format (jpg, png, gif, tiff, etc.) and serves webp or avif based on the browser's
Acceptheader. No manual conversion needed. - Responsive
srcsetgeneration: creates multiple image sizes from configured breakpoints so the browser can pick the best fit. - Lazy loading by default: images load when they approach the viewport, using the native
loading="lazy"attribute. - Aspect ratio reservation: when you provide
widthandheight, the component calculates the aspect ratio and reserves space in the layout before the image loads. This prevents Cumulative Layout Shift (CLS). More on that in my Core Web Vitals article. - On-demand optimization: images are processed at request time, not during the build. This means even remote images from a CMS get optimized without a build step.
The sizes prop
This is the most impactful prop that a lot of teams miss, unfortunately because the prop is not required.
Without sizes, the browser assumes the image takes up the full viewport width and downloads an image sized accordingly. If your layout renders that image at 33% width on desktop, the browser is downloading an image roughly 3x wider than what gets displayed. Because file size scales with the square of the dimensions, that can mean downloading up to 9x more data than necessary.
Setting sizes correctly tells the browser how wide the image will actually be at each breakpoint:
<Image
src="/product.jpg"
alt="Product"
width={800}
height={600}
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
This tells the browser: full width on mobile, half width on tablets, one-third on desktop. The browser then picks the smallest image from the srcset that covers the actual display size. On a desktop screen, that could be the difference between downloading a 400KB image and a 3.6MB one. Multiply that across every image on every page load, and you're looking at significantly higher bandwidth costs and slower experiences for users on mobile connections.
Preloading critical images
For images above the fold, especially the LCP element, you want the opposite of lazy loading. You want the browser to start fetching as early as possible.
The Next.js docs recommend two approaches depending on your situation. For most cases, loading="eager" or fetchPriority="high" is enough:
<Image
src="/hero.jpg"
alt="Hero"
width={1200}
height={600}
loading="eager"
fetchPriority="high"
/>
loading="eager" tells the browser to load the image immediately regardless of viewport position. fetchPriority="high" goes a step further and hints that this image should be prioritized over other resources in the loading queue.
For cases where the image needs to start loading before the browser even encounters the <img> element in the DOM, use the preload prop (renamed from priority in Next.js 16 to make the behavior explicit). This injects a <link rel="preload"> tag into the document's <head>:
<Image
src="/hero.jpg"
alt="Hero"
width={1200}
height={600}
preload
/>
Note that preload automatically sets fetchPriority="high" on the image, so there's no need to add both.
Use preloading only for above-the-fold images. One, maybe two per page. Preloading everything defeats the purpose and creates bandwidth competition with other critical resources.
Don't make it jump around
Every image needs defined dimensions, or it will cause layout shifts when it loads. The next/image component gives you two options:
When you know the dimensions, provide width and height. The component uses them to calculate an aspect ratio and reserve space in the layout before the image arrives.
When dimensions are unknown or dynamic (user-uploaded content, CMS images with variable sizes), use the fill prop instead. The image expands to fill its parent container, which must have position: relative. Combined with object-fit: cover, this handles most dynamic image scenarios cleanly.
Both approaches prevent CLS by ensuring the browser knows how much space to allocate before the image data arrives.
A component that keeps evolving
The next/image component has changed significantly over its lifetime, and Next.js 16's priority to preload rename is just the latest iteration. The browser primitives it builds on keep improving too. What required build plugins and custom JavaScript a few years ago is now a prop on a component.
If you're tracking the impact of these optimizations on real user experience, I wrote about how to measure Core Web Vitals in Next.js.