Images are often one of the biggest contributors to page size and loading time in modern web applications. A poorly optimized image can significantly increase Largest Contentful Paint (LCP), consume unnecessary bandwidth, and make a page feel slow—especially on mobile devices.
Next.js provides the built-in Image component, which handles much of the complexity of modern image optimization for you. In this article, we’ll explore how to use the Image component in Next.js 16, including:
- Basic image rendering
- Local and remote images
- Responsive images
- width and height
- fill
- sizes
- Lazy loading
- Image quality
- Image formats
- Remote image configuration
- Background-like images
Why Use the Next.js Image Component?
Instead of using a normal <img /> tag:
<img src="/images/product.jpg" alt="Product" />
Nuxt provides:
import Image from "next/image"; <Image src="/images/product.jpg" alt="Product" />
The Image component can automatically optimize images by:
- Serving appropriately sized images.
- Automatically generating optimized image variants.
- Supporting modern formats such as WebP and AVIF where appropriate.
- Lazy-loading images by default when suitable.
- Preventing layout shifts when dimensions are known.
- Working with responsive image sizes.
- Optimizing remote images when they are explicitly configured.
The important point is that you don’t have to manually create multiple versions of every image.
Basic Image Rendering
A simple locale image can rendered like this:
import Image from "next/image";
export default function Profile() {
return (
<Image
src="/images/profile.jpg"
width={400}
height={400}
alt="John Doe"
/>
);
}
The image located at:
public/images/profile.jpg
The browser access it using:
/images/profile.jpg
So the project structure look like this:
my-app/ ├── app/ │ └── page.jsx ├── public/ │ └── images/ │ └── profile.jpg └── package.json
Why Height and width matters
You will commonly see:
<Image
src="/images/product.jpg"
width={800}
height={600}
alt="Product"
/>
These values describe the image’s intrinsic dimensions/aspect ratio.
They aren’t necessarily saying: “Always render this image at exactly 800 × 600 pixels.”
Instead, they allow Next.js and the browser to understand the image’s dimensions and reserve the appropriate space.
This helps prevent Cumulative Layout Shift (CLS).
For example: without dimensions
<img src="/product.jpg" alt="Product" />
the browser may initially have no idea how much vertical space to reserve. When the image loads, the page can shift.
However with:
<Image
src="/product.jpg"
width={800}
height={600}
alt="Product"
/>
the browser knows the aspect ratio ahead of time.
Importing Local Images
Next.js allows you to import images directly like any other component:
For example:
import Image from "next/image";
import productImage from "@/public/images/product.jpg";
export default function Product() {
return (
<Image
src={productImage}
alt="Product"
/>
);
}
In this case Next.js can determine image metadata such as:
- width
- height
- source
So you don’t need to manually specify them:
For example:
<Image
src={productImage}
alt="Product"
/>
Is often preferable to:
<Image
src={productImage}
width={800}
height={600}
alt="Product"
/>
when using statically imported local images.
Responsive Images
One of the most important features of the Image component is responsive image delivery.
Consider a product card:
<div className="product-card">
<Image
src="/images/product.jpg"
width={500}
height={500}
alt="Product"
/>
</div>
Suppose that the image is actually displayed at:
- 200px on mobile
- 300px on tablet
- 400px on desktop
Sending a 2000px-wide image to every device is wasteful.
Instead, you can use sizes prop:
<Image
src="/images/product.jpg"
width={800}
height={800}
sizes="(max-width: 768px) 50vw, 25vw"
alt="Product"
/>
The sizes prop tells the browser approximately how much viewport width the image occupies.
For example:
Mobile: 50vw Desktop: 25vw
This allows the browser to select an appropriately sized image.
How Image Sizes Work
Consider:
<Image
src="/images/product.jpg"
width={1200}
height={800}
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
alt="Product"
/>
This means:
Viewport <= 768px
→ image occupies approximately 100% viewport width
Viewport <= 1200px
→ image occupies approximately 50% viewport width
Larger screens
→ image occupies approximately 33% viewport width
Which is very useful for things such as:
- product grids
- blog cards
- galleries
- listing pages
Using the fill property
Sometimes you don’t know the image’s exact dimensions.
For example, you may have a card:
<div className="card">
<Image
src="/images/property.jpg"
fill
alt="Property"
/>
</div>
The parent needs a defined positioning context:
.card {
position: relative;
width: 100%;
height: 300px;
}
Now the image fills the parents.
Also you can use css object-fit with the fill property:
<Image
src="/images/property.jpg"
fill
alt="Property"
style={{
objectFit: "cover",
}}
/>
Or with css:
.image {
object-fit: cover;
}
Then:
<Image src="/images/property.jpg" fill className="image" alt="Property" />
cover means the image fills the container while maintaining its aspect ratio. Some parts of the image may be cropped.
While if we use object-fit: contain: For items where the entire object must remain visible:
<Image
src="/images/laptop.png"
fill
alt="Laptop"
style={{
objectFit: "contain",
}}
/>
This is useful for:
- product images
- logos
- icons
- transparent PNGs
Unlike cover, contain doesn’t crop the image.
Lazy Loading Images
Images outside the initial viewport can generally be lazy-loaded.
For example:
<Image
src="/images/product.jpg"
width={600}
height={600}
alt="Product"
loading="lazy"
/>
The browser doesn’t need to download every image immediately. With the loading prop set it to lazy. The default value is eager which means load the image immediately regardless if it’s in the viewport of not.
Image a product listing with 100 product:
Product 1 Product 2 Product 3 ... Product 100
If only the first few are visible, loading all 100 images immediately would waste bandwidth.
Lazy loading allows images further down the page to load when they’re needed.
The Image component already provides sensible lazy-loading behavior by default, so you usually don’t need to specify it manually.
Don’t Preload Everything
A common mistake is doing this:
<Image src="/product-1.jpg" preload ... /> <Image src="/product-2.jpg" preload ... /> <Image src="/product-3.jpg" preload ... />
This defeats the purpose of prioritization.
Instead:
<Image src="/hero.jpg" preload ... />
and allow normal images to load lazily.
Think of image priority as a limited resource.
Remote Images
Suppose your backend returns:
https://cdn.example.com/products/iphone.jpg
You cannot simply use any arbitrary remote URL without configuring it.
You might write:
<Image
src="https://cdn.example.com/products/iphone.jpg"
width={800}
height={800}
alt="iPhone"
/>
but the remote host needs to be allowed in your Next.js configuration.
Configuring Remote Images
In next.config.js, configure the remote image host.
For example:
const nextConfig = {
images: {
remotePatterns: [
{
protocol: "https",
hostname: "cdn.example.com",
pathname: "/products/**",
},
],
},
};
export default nextConfig;
Now the remote image will load.
The advantage of remotePatterns is that you can restrict exactly which remote URLs are allowed instead of allowing an entire arbitrary domain.
Example: loading images from a Laravel based backend:
Suppose Laravel api return:
{
"id": 10,
"name": "Apartment",
"image": "https://api.example.com/storage/properties/apartment.jpg"
}
Your Next.js component can do:
<Image
src={property.image}
width={800}
height={600}
alt={property.name}
/>
And configure:
const nextConfig = {
images: {
remotePatterns: [
{
protocol: "https",
hostname: "api.example.com",
pathname: "/storage/properties/**",
},
],
},
};
export default nextConfig;
Another Example: loading images from a CDN:
If your images stored in a CDN like this:
https://cdn.example.com/images/...
you can configure that CDN as a remote image source.
And the remotePatterns will be:
const nextConfig = {
images: {
remotePatterns: [
{
protocol: "https",
hostname: "cdn.example.com",
pathname: "/images/**",
},
],
},
};
export default nextConfig;
Then:
<Image
src={imageUrl}
width={1200}
height={800}
alt="Property"
/>
Image Quality
You may specify an image quality value:
<Image
src="/images/property.jpg"
width={1200}
height={800}
quality={75}
alt="Property"
/>
Higher quality usually means:
Better visual quality
↓
Larger file
↓
More bandwidth
Use higher values only when needed.
Webp and AVIF
Modern image formats can significantly reduce file sizes compared with traditional JPEG and PNG.
You can configure the formats Next.js should use:
const nextConfig = {
images: {
formats: ["image/avif", "image/webp"],
},
};
export default nextConfig;
The browser’s supported format is considered when serving the optimized image.
WebP is also widely supported and remains a strong general-purpose choice.
Images inside Server Components
One of the nice things about Next.js is that Image works naturally inside Server Components.
For example:
import Image from "next/image";
export default async function ProductPage() {
const product = await getProduct();
return (
<div>
<Image
src={product.image}
width={800}
height={800}
alt={product.name}
/>
<h1>{product.name}</h1>
</div>
);
}
You don’t need to make the component a Client Component just because you’re rendering an image.
A practical Optimization Strategy
For most applications, you can follow this strategy:
Hero Image
<Image
src="/hero.jpg"
fill
preload
sizes="100vw"
alt="..."
style={{ objectFit: "cover" }}
/>
Product/listing card
<Image
src={image}
fill
sizes="(max-width: 768px) 100vw, 33vw"
alt={title}
style={{ objectFit: "cover" }}
/>
Product Image
<Image
src={image}
fill
sizes="(max-width: 768px) 50vw, 25vw"
alt={name}
style={{ objectFit: "contain" }}
/>
Content Image
<Image
src={image}
width={1200}
height={800}
sizes="(max-width: 768px) 100vw, 800px"
alt="..."
/>
Example: Property Card:
import Image from "next/image";
export default function PropertyCard({ property }) {
return (
<article className="property-card">
<div className="property-image">
<Image
src={property.image}
alt={property.title}
fill
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
style={{
objectFit: "cover",
}}
/>
</div>
<div className="property-content">
<h2>{property.title}</h2>
<p>{property.location}</p>
<strong>{property.price}</strong>
</div>
</article>
);
}
CSS:
.property-card {
overflow: hidden;
border-radius: 12px;
}
.property-image {
position: relative;
width: 100%;
aspect-ratio: 16 / 10;
}
.property-content {
padding: 16px;
}
This gives you:
- responsive sizing
- preserved aspect ratio
- optimized image delivery
- lazy loading behavior
- correct cropping
- appropriate image sizes for different screens


