DEV Community

Cover image for How Next.js Image Optimization Works Under the Hood
Harsh Mangalam
Harsh Mangalam

Posted on

How Next.js Image Optimization Works Under the Hood

When I first started using Next.js, I replaced every <img> with <Image /> because the documentation said it was "optimized."

It worked.

But I had one question:

What is it actually doing behind the scenes?

After digging into it, I realized the idea is much simpler than I expected.

Let's walk through it together.


The problem with a normal <img>

Imagine you have a beautiful travel photo.

taj-mahal.png
4000 × 3000
6 MB
Enter fullscreen mode Exit fullscreen mode

You display it on your website like this:

<img src="/taj-mahal.png" />
Enter fullscreen mode Exit fullscreen mode

When someone opens your page, the browser asks the server for that image.

Browser
   │
   ▼
GET /taj-mahal.png
   │
   ▼
Server sends 6 MB image
Enter fullscreen mode Exit fullscreen mode

The browser downloads the entire file.

Now imagine your image is only shown inside a small card that's 300 pixels wide.

The browser still downloads the full 4000-pixel image.

That means you're downloading way more data than you actually need.


What would be the smarter approach?

Instead of sending the same image to everyone, what if we could do this?

For phones:

300 px image
Enter fullscreen mode Exit fullscreen mode

For tablets:

600 px image
Enter fullscreen mode Exit fullscreen mode

For desktops:

1000 px image
Enter fullscreen mode Exit fullscreen mode

Everyone sees the same picture, but only downloads the size they actually need.

That is the main idea behind image optimization.


Enter Next.js <Image />

Instead of writing

<img src="/taj-mahal.png" />
Enter fullscreen mode Exit fullscreen mode

we write

<Image
  src="/taj-mahal.png"
  width={500}
  height={300}
/>
Enter fullscreen mode Exit fullscreen mode

Looks almost the same.
But behind the scenes, something very different happens.


The first surprise

You might think the browser requests this:

GET /taj-mahal.png
Enter fullscreen mode Exit fullscreen mode

It doesn't.

Instead, it requests something like this:

GET /_next/image?url=/taj-mahal.png&w=640&q=75
Enter fullscreen mode Exit fullscreen mode

At first, this looked strange to me.

Why isn't it requesting the actual image?


Meet the Image Optimizer

That /_next/image URL is a special endpoint created by Next.js.

Instead of sending the original image directly, the request goes through the optimizer first.

Browser
    │
    ▼
Next.js Image Optimizer
    │
    ▼
Original Image
Enter fullscreen mode Exit fullscreen mode

You can think of it as a smart middleman.

Before giving the browser the image, it asks:

  • What size do you need?
  • What quality should I use?
  • Which image format does your browser support?

Then it prepares the best version.


What happens when the request arrives?

Suppose the browser sends:

GET /_next/image?url=/taj-mahal.png&w=640&q=75
Enter fullscreen mode Exit fullscreen mode

Next.js now knows:

  • Original image location
  • Required width (640px)
  • Image quality (75)

Now the real work begins.


Step 1 — Load the original image

Next.js finds the original image.

It might come from:

  • your public folder
  • an external URL
  • cloud storage like S3

The optimizer first loads the original image into memory.


Step 2 — Resize it

Suppose the original image is

4000 px wide
Enter fullscreen mode Exit fullscreen mode

But the browser only needs

640 px
Enter fullscreen mode Exit fullscreen mode

There is no reason to send all 4000 pixels.

Next.js resizes the image to the requested width.

4000 px

↓

640 px
Enter fullscreen mode Exit fullscreen mode

This alone saves a lot of bandwidth.


Step 3 — Compress it

Even after resizing, the image can often be made much smaller.

If the quality is set to 75, Next.js compresses it while trying to keep it looking almost identical.

For example:

400 KB

↓

90 KB
Enter fullscreen mode Exit fullscreen mode

The user usually can't notice the difference, but their internet connection definitely can.


Step 4 — Choose a better format

Modern browsers support formats like:

  • WebP
  • AVIF

These formats are usually much smaller than PNG or JPEG.

When your browser makes a request, it also sends an Accept header that tells the server which image formats it understands.

For example:

Accept: image/avif,image/webp,image/*,*/*
Enter fullscreen mode Exit fullscreen mode

If the browser supports WebP or AVIF, Next.js may convert the original PNG or JPEG into one of those formats before sending it back.

Original

PNG

↓

Optimized

WebP
Enter fullscreen mode Exit fullscreen mode

The picture looks almost identical but downloads much faster.


Step 5 — Cache the result

Image optimization isn't free.

Resizing and compressing images takes CPU time.

So Next.js stores the optimized version.

The first request looks like this:

Browser

↓

Next.js

↓

Resize

↓

Compress

↓

Cache

↓

Send image
Enter fullscreen mode Exit fullscreen mode

The second request becomes much simpler.

Browser

↓

Next.js

↓

Cache

↓

Send image
Enter fullscreen mode Exit fullscreen mode

No resizing.

No compression.

Just return the cached image.


How does the browser know which size to download?

This part surprised me the most.

Next.js doesn't generate just one image.

It generates multiple versions.

Something like:

320 px

640 px

750 px

1080 px

1920 px
Enter fullscreen mode Exit fullscreen mode

It then generates an HTML srcset attribute.

Something similar to this:

<img
  src="/_next/image?url=/taj-mahal.png&w=640&q=75"
  srcset="
    /_next/image?url=/taj-mahal.png&w=320&q=75 320w,
    /_next/image?url=/taj-mahal.png&w=640&q=75 640w,
    /_next/image?url=/taj-mahal.png&w=1080&q=75 1080w
  "
  sizes="100vw"
/>
Enter fullscreen mode Exit fullscreen mode

Now here's the interesting part.

Next.js doesn't decide which image gets downloaded.

The browser does.

If you're on a phone, it might download the 640px version.

If you're on a desktop with a Retina display, it might choose the 1080px version.

The browser looks at:

  • Viewport size
  • Device pixel ratio (Retina, 2x, 3x, etc.)
  • The sizes attribute

Then it picks the most appropriate image.


Why do we provide width and height?

One more nice feature.

When you write

<Image
  width={800}
  height={600}
/>
Enter fullscreen mode Exit fullscreen mode

the browser already knows how much space the image will take.

Even before the image finishes downloading.

That means your page doesn't suddenly jump around while loading.

Without dimensions:

Header

Text

(Image loads)

Everything moves down
Enter fullscreen mode Exit fullscreen mode

With dimensions:

Header

[Reserved Image Space]

Text
Enter fullscreen mode Exit fullscreen mode

Everything stays stable.

This improves a Core Web Vitals metric called Cumulative Layout Shift (CLS).


Putting everything together

<Image />

      
      

Browser requests

/_next/image

      
      

Next.js loads the original image

      
      

Resize

      
      

Compress

      
      

Convert to WebP/AVIF (if supported)

      
      

Store in cache

      
      

Send optimized image to the browser
Enter fullscreen mode Exit fullscreen mode

Final thoughts

Before learning this, I thought <Image /> was just another React component.

It turns out it's much more than that.

It's an image optimization pipeline built into Next.js.

Instead of sending every visitor the same large image, it creates a version that's appropriate for their device, browser, and screen size.

As developers, we only write one component.

Behind the scenes, Next.js handles resizing, compression, modern image formats, caching, and responsive images for us.

That's a lot of work hidden behind a single line of code.


If you enjoyed this article, the next logical step is understanding how srcset and sizes actually work. That's where the browser starts making intelligent decisions about which image to download, and it's one of the coolest parts of responsive images.

Top comments (0)