The Next.js Image component is actually a thin client-side wrapper around a powerful server-side image optimization service.

Let’s see it in action. Imagine you have a Next.js app and you want to display an image.

// pages/index.js
import Image from 'next/image';
import myImage from '../public/hero.jpg';

function HomePage() {
  return (
    <div>
      <h1>Welcome to my site!</h1>
      <Image
        src={myImage}
        alt="Hero image"
        width={500}
        height={300}
      />
    </div>
  );
}

export default HomePage;

When this page loads in a browser, the Image component doesn’t directly render an <img> tag with your original hero.jpg. Instead, it renders something like this:

<img
  alt="Hero image"
  src="/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fhero.xxxxxxxx.jpg&w=1024&q=75"
  decoding="async"
  data-nimg="1"
  style="color: transparent;"
/>

Notice the src. It’s pointing to a special /_next/image endpoint. This is where Vercel’s (or your self-hosted Next.js server’s) image optimization kicks in. The query parameters tell the server what we want: url is the original image path, w is the desired width, and q is the quality.

The server-side service then takes your hero.jpg, resizes it to 1024px wide (the largest size requested or available), compresses it to 75% quality, and serves it back. It also automatically detects the best image format (like WebP if the browser supports it) and delivers that. The data-nimg="1" attribute is a signal to the client-side library that this image is managed by the next/image component.

This whole process solves a few critical problems for modern web applications. First, it dramatically improves performance by serving appropriately sized images for the user’s viewport. No more downloading a massive 4MB image on a mobile device. Second, it handles modern image formats like WebP, which offer better compression than JPEG or PNG, leading to smaller file sizes and faster load times. Third, it provides lazy loading out-of-the-box, meaning images below the fold aren’t loaded until the user scrolls to them, further speeding up initial page load.

The core mental model is that next/image is a request for an optimized image, and the /_next/image endpoint is the service fulfilling that request. You declare your intent (desired width, height, quality), and the service delivers the best possible version.

You control this primarily through the props on the Image component. width and height are crucial; they define the aspect ratio and are used by the service to determine the maximum size to generate. If you omit width and height and use layout="fill", the image will scale to fill its parent container, and the optimization service will dynamically generate sizes based on the actual rendered size. The quality prop (1-100, default 75) directly influences the compression level. priority={true} tells Next.js to prefetch the image and load it above the fold with higher priority.

Under the hood, when you use next/image with a local file (import myImage from '../public/hero.jpg'), Next.js builds your project, hashes the image file, and places it in the .next/static/media directory. The /_next/image endpoint then uses this hashed path to find the original image and optimize it. If you use an external URL (src="https://example.com/my-image.jpg"), the optimization service will fetch the image from that URL on demand.

The image optimization service generates multiple image sizes on the fly, not just one. When the browser requests an image via /_next/image?url=...&w=...&q=..., the server calculates the appropriate size based on the w parameter and the available generated sizes (or generates it on the fly if it hasn’t been requested before). It then serves the most appropriate size and format. The srcset attribute on the generated <img> tag is populated with various sizes, allowing the browser to pick the best one based on its own rendering capabilities and screen density.

The most surprising thing to many is that the width and height props are not just for aspect ratio; they are fundamental to how the optimization service determines the maximum size of an image it will serve. If you set width={100} and height={100} but the image is actually displayed much larger by CSS, the optimization service will still only generate images up to 100px in width, leading to pixelation. The layout="responsive" or layout="fill" options, combined with appropriate parent container sizing, are key to getting dynamic resizing correct.

The next concept you’ll likely encounter is handling images from external sources more robustly, including configuring allowed domains and dealing with caching strategies for those remote images.

Want structured learning?

Take the full Vercel course →