Images are everywhere in modern web applications, but they're often treated as an afterthought. I've seen teams ship React and Next.js apps with unoptimized images that tank performance, drain user data, and cause layout shifts that feel janky. Getting next/image, properly requires understanding how the Image component's props interact with responsive design, lazy loading, and Core Web Vitals.
Why next/image matters
Before diving into implementation details, let's acknowledge the problem. HTML's native <img> tag doesn't optimize images by default. Browsers download the full resolution you serve, regardless of screen size or connection speed. This leads to bloated bundle sizes, slow page loads, and poor Core Web Vitals scores—all directly impacting SEO and user experience.
Next.js's <Image> component (from next/image) solves this by automatically optimizing images at build time and serving them in modern formats like WebP. But the component only works well when you configure it correctly. Miss a few key settings, and you'll get minimal benefits or even introduce new problems.
Always set sizes — Without sizes, the browser may download a much larger image than needed
The sizes prop is the most misunderstood setting in next/image. When you omit it, Next.js defaults to 100vw, meaning the browser assumes your image will stretch to full viewport width. On mobile, this triggers download of a desktop-sized image, wasting bandwidth and slowing the page.
The sizes attribute tells the browser which image width to fetch based on viewport breakpoints. It uses media queries to describe how an image will render at different screen sizes, letting the browser download only what it needs.
// ❌ Bad: No sizes prop. Browser downloads large image on mobile.
<Image
src="/product.jpg"
alt="Product photo"
fill
/>
// ✅ Good: Explicit sizes for responsive rendering
<Image
src="/product.jpg"
alt="Product photo"
fill
sizes="(max-width: 768px) 50vw, 25vw"
/>In this example, on screens ≤768px, the image renders at 50% of viewport width. On larger screens, it's 25vw. The browser then selects the smallest optimized image that covers both scenarios, reducing file size significantly.
I worked on an e-commerce platform where product grids showed 4 items per row on desktop and 2 on mobile. Without proper sizes, mobile devices were downloading the full 1200px desktop image instead of a 300px version. Adding sizes="(max-width: 768px) 50vw, (max-width: 1024px) 33vw, 25vw" cut mobile image bytes by 60% immediately.
The syntax follows a pattern: (media-query) width, (media-query) width, ... default-width. Always provide a default at the end with no media query. Common units are vw (viewport width), px, or em.
// Hero image: full width on mobile, constrained on desktop
<Image
src="/hero.jpg"
alt="Hero banner"
fill
sizes="100vw"
style={{ objectFit: "cover" }}
/>
// Sidebar widget: always 200px
<Image
src="/widget.jpg"
alt="Sidebar widget"
width={200}
height={200}
sizes="200px"
/>
// Card in a 3-column grid
<Image
src="/card.jpg"
alt="Card"
fill
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
/>Priority for LCP — Mark the hero image with priority so it loads first
Largest Contentful Paint (LCP) is one of Google's Core Web Vitals, and it measures when the largest visible element appears on screen. For most pages, that's the hero image. If it loads slowly, your LCP score suffers, hurting rankings and user experience.
By default, images are lazy-loaded, meaning they only start downloading when they're about to enter the viewport. For above-the-fold images, this delay is wasted. The priority prop tells Next.js to preload the image as early as possible, removing unnecessary latency.
// ❌ Bad: Hero image lazy-loads like a footer image
<Image
src="/hero.jpg"
alt="Hero banner"
fill
sizes="100vw"
/>
// ✅ Good: Hero image preloads immediately
<Image
src="/hero.jpg"
alt="Hero banner"
fill
sizes="100vw"
priority
/>When priority={true}, Next.js adds a <link rel="preload"> tag in the document head, and the browser fetches the image before rendering the page. For images outside the viewport (like a carousel's second slide), use lazy loading instead.
On a travel booking platform, I applied priority to the destination hero image but left carousel thumbnails with lazy loading. LCP improved by ~400ms, and we didn't unnecessarily download off-screen images.
Use priority sparingly. Preloading too many images defeats the purpose and bloats the initial document. Typically, only the single most important image per page needs it.
// In a product detail page
export default function ProductPage({ product }) {
return (
<>
{/* Hero image: priority for LCP */}
<Image
src={product.mainImage}
alt={product.name}
fill
priority
sizes="100vw"
/>
{/* Thumbnails: lazy load */}
<div>
{product.images.map((img, idx) => (
<Image
key={idx}
src={img}
alt={`${product.name} view ${idx + 1}`}
width={100}
height={100}
/>
))}
</div>
</>
);
}Width and height — Prevent layout shift by reserving space
Layout shift happens when elements move on the page after content loads. If an image has no reserved space, the browser flows other content into that area, then reflows when the image arrives. This is annoying for users and harms Cumulative Layout Shift (CLS), another Core Web Vital.
Setting width and height (or using fill with a container of known aspect ratio) tells the browser to reserve space for the image before it downloads, preventing reflow.
// ❌ Bad: No dimensions. Text shifts when image loads.
<Image src="/photo.jpg" alt="Photo" />
// ✅ Good: Explicit dimensions reserve space
<Image
src="/photo.jpg"
alt="Photo"
width={400}
height={300}
/>
// ✅ Also good: fill with aspect-ratio container
<div style={{ position: "relative", width: "400px", aspectRatio: "4/3" }}>
<Image
src="/photo.jpg"
alt="Photo"
fill
/>
</div>When you provide width and height, Next.js calculates the aspect ratio and generates a CSS rule that reserves space. The image loads into that reserved area without pushing content around.
I've debugged CLS issues on SaaS platforms where profile images in tables had no dimensions. Users clicked a button, the image loaded, and the entire row shifted. Adding width and height fixed it instantly.
If your image dimensions vary (e.g., a grid where items have different aspect ratios), use a container with a known aspect ratio and the fill prop. This is cleaner than hardcoding dimensions for every variant.
// Variable-sized images in a masonry grid
function ImageCard({ image }) {
const aspectRatio = image.width / image.height;
return (
<div
style={{
position: "relative",
width: "100%",
aspectRatio: `${aspectRatio}`
}}
>
<Image
src={image.url}
alt={image.alt}
fill
sizes="(max-width: 640px) 100vw, 50vw"
/>
</div>
);
}Remote patterns — Allow-list external image hosts in next.config
Next.js optimizes images locally at build time if they're in your public folder, but external images (from CDNs, APIs, or third-party services) are optimized on-demand. To serve remote images, you must allow-list their hosts in next.config.js.
Without this configuration, Next.js blocks external image hosts by default, returning an error. This is a security feature—it prevents your server from being abused as a proxy for arbitrary URLs.
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
remotePatterns: [
{
protocol: "https",
hostname: "api.example.com",
pathname: "/images/**",
},
{
protocol: "https",
hostname: "*.cloudinary.com",
},
{
protocol: "https",
hostname: "images.unsplash.com",
},
],
},
};
module.exports = nextConfig;Each object in remotePatterns specifies a host and optional path and protocol. You can use wildcards in hostname (e.g., *.cloudinary.com) to match subdomains. The pathname field lets you restrict to specific paths if needed.
On a CRM platform, we pulled user avatars from Auth0 and product images from Cloudinary. I added both hosts to remotePatterns, and Next.js optimized them automatically on first request and cached the result.
Be specific when possible. Instead of allowing all hosts with a wildcard, list only the services you actually use. This prevents accidentally serving images from untrusted sources.
// Good: Allow specific CDN and API hosts
images: {
remotePatterns: [
{ protocol: "https", hostname: "cdn.myapp.com" },
{ protocol: "https", hostname: "user-uploads.myapp.com" },
],
}
// Questionable: Allow all subdomains of a domain
images: {
remotePatterns: [
{ protocol: "https", hostname: "**.example.com" },
],
}
// Bad: Allow everything (security risk)
images: {
remotePatterns: [
{ protocol: "https", hostname: "*" },
],
}Common mistakes
- Omitting sizes with fill: When using
fill, always includesizes. Without it, Next.js assumes 100vw, and the browser downloads oversized images on mobile. - Using priority on multiple images: Only the single most important image needs
priority. Using it on 5 images defeats preloading optimization and slows page load. - No width/height with fixed dimensions: If you know the image dimensions, always set
widthandheightto prevent layout shift. This is especially important for images in lists or grids. - Forgetting remotePatterns: Forgetting to allow-list a CDN host will cause silent failures (images won't load in production) or cryptic errors. Check next.config.js early when debugging missing external images.
- Bloated sizes strings: Keep
sizessimple and aligned with your CSS breakpoints. Over-complicated sizes don't improve optimization and are hard to maintain. - Using fill without container positioning: When using
fill, the parent must haveposition: relative(orabsolute,fixed). Otherwise, the image won't render.
Key takeaways
- Always set
sizeswithfillto let the browser download right-sized images for each viewport. Without it, mobile users download desktop images unnecessarily. - Use
priorityon exactly one hero image per page to improve LCP. Don't mark multiple images as priority. - Set
widthandheight(or usefillwith aspect-ratio container) to reserve space and prevent Cumulative Layout Shift. - Allow-list external image hosts in
next.config.jsunderremotePatterns. Be specific—restrict to the services you actually use. - Combine these settings for best results. A well-optimized image uses
fill,sizes, proper dimensions, andpriority(if above-the-fold), plus proper remote pattern configuration.






Comments
Loading comments…