Next.js Interview Prep
Performance and Optimization

Image Optimization

next/image — Stop Shipping 4MB Hero Images to Mobile Users

LinkedIn Hook

"Our Lighthouse score was 38. The homepage had six images — none of them optimized."

No lazy loading. No responsive sizing. Full-resolution PNGs served to phones on 3G. The Largest Contentful Paint was 7.8 seconds.

We replaced every <img> tag with next/image and scored 94 — without touching a single image file, without installing a plugin, without configuring a CDN.

Most developers know next/image exists. Very few understand why it requires width and height, when to use fill instead, or how the sizes prop actually controls what gets downloaded.

In Lesson 7.1, you'll learn how Next.js image optimization works under the hood, how to configure it for remote images, and the exact props interviewers ask about.

Read the full lesson -> [link]

#NextJS #ImageOptimization #WebPerformance #CoreWebVitals #LCP #FrontendDevelopment #InterviewPrep #React


Image Optimization thumbnail


What You'll Learn

  • Why next/image exists and the five problems it solves automatically
  • Why width and height are required and what happens if you skip them
  • How the fill prop works and when to use it instead of explicit dimensions
  • What the sizes prop actually does and how it controls which image the browser downloads
  • How to use priority on LCP images and why it matters for Core Web Vitals
  • How blur placeholders work and how to generate them
  • How to configure next.config.js for remote images and custom loaders

The Photo Printing Analogy — Don't Print a Billboard for a Postcard

Imagine you run a photo printing shop. A customer comes in and says, "I need a 4x6 inch photo for a greeting card."

The naive approach (plain <img>): You take their original 8000x6000 pixel photograph — a 12MB file meant for billboard printing — and you print it at 4x6 inches. The print looks fine, but you just wasted an enormous amount of ink, paper processing time, and storage. Worse, if the customer wanted a wallet-sized version, you'd still print from the same 12MB file.

The smart approach (next/image): You check the customer's order size, generate a version that's exactly the right resolution for a 4x6 print (maybe 1200x900 pixels, 150KB), and deliver that. If another customer wants a wallet-sized version, you generate a tiny 300x200 version (20KB). You also check if their printer supports the latest high-efficiency ink (WebP/AVIF) and use that instead of traditional ink (PNG/JPEG) when possible.

That's what next/image does. It sits between your original images and the browser, and it automatically serves the right size, in the right format, at the right time.


The Five Problems next/image Solves

Before diving into props and configuration, you need to understand why Next.js built a custom image component. The native <img> tag has five critical problems for web performance:

Problem 1: No Automatic Format Conversion

Browsers support modern, highly compressed formats like WebP (30% smaller than JPEG) and AVIF (50% smaller). But if you serve a PNG, the browser downloads the PNG — it won't convert it for you. next/image automatically detects the browser's supported formats via the Accept header and serves the most efficient one.

Problem 2: No Responsive Sizing

A plain <img src="hero.png"> sends the same 2400px-wide image to a 320px-wide phone screen. The user downloads 10x more data than they need. next/image generates multiple sizes and uses srcset to let the browser pick the right one.

Problem 3: No Lazy Loading by Default

Every <img> tag starts downloading immediately when the HTML parses, even if the image is 3000 pixels below the fold. next/image adds loading="lazy" by default, so off-screen images only load when the user scrolls near them.

Problem 4: Layout Shift (CLS)

When an image loads without reserved space, the page content jumps around — a layout shift. This hurts Cumulative Layout Shift (CLS), a Core Web Vital. next/image requires dimensions so the browser reserves the exact space before the image loads.

Problem 5: No Caching or Optimization Pipeline

Serving optimized images typically requires a CDN, an image processing service, or a build-time plugin. next/image includes a built-in image optimization API at /_next/image that resizes, reformats, and caches images on the fly — no external service needed.

+----------------------------------------------------+
|        Plain <img> vs next/image                    |
+----------------------------------------------------+
|                                                    |
|  <img>:                                            |
|    - Serves original file (2MB PNG)                |
|    - Same file to all devices                       |
|    - Loads immediately (even off-screen)            |
|    - No space reserved (layout shift)               |
|    - No format conversion                           |
|                                                    |
|  <Image>:                                          |
|    - Serves optimized file (80KB WebP)              |
|    - Different sizes per viewport                   |
|    - Lazy loads by default                          |
|    - Space reserved (no layout shift)               |
|    - Auto WebP/AVIF conversion                      |
|                                                    |
+----------------------------------------------------+

next/image vs img — A Side-by-Side Comparison

Here is the fundamental difference in code:

// The old way — plain HTML img tag
// Problem: no optimization, no lazy loading, no responsive sizing, layout shift
export default function HeroSection() {
  return (
    <div>
      <img
        src="/images/hero-banner.png"
        alt="Product showcase"
        // No width/height = layout shift
        // Full-size PNG sent to all devices
        // Loads immediately even if off-screen
      />
    </div>
  );
}
// The Next.js way — optimized Image component
import Image from 'next/image';

export default function HeroSection() {
  return (
    <div>
      <Image
        src="/images/hero-banner.png"
        alt="Product showcase"
        width={1200}
        height={630}
        // Automatic: lazy loading, WebP/AVIF, srcset, reserved space
        // Browser receives exactly the size it needs
      />
    </div>
  );
}

// What actually gets rendered in the HTML:
// <img
//   alt="Product showcase"
//   loading="lazy"
//   width="1200"
//   height="630"
//   decoding="async"
//   srcset="
//     /_next/image?url=%2Fimages%2Fhero-banner.png&w=640&q=75 640w,
//     /_next/image?url=%2Fimages%2Fhero-banner.png&w=750&q=75 750w,
//     /_next/image?url=%2Fimages%2Fhero-banner.png&w=828&q=75 828w,
//     /_next/image?url=%2Fimages%2Fhero-banner.png&w=1080&q=75 1080w,
//     /_next/image?url=%2Fimages%2Fhero-banner.png&w=1200&q=75 1200w,
//     /_next/image?url=%2Fimages%2Fhero-banner.png&w=1920&q=75 1920w
//   "
//   src="/_next/image?url=%2Fimages%2Fhero-banner.png&w=1920&q=75"
// />

The next/image component transforms a single image source into an entire pipeline: multiple sizes, modern formats, lazy loading, and CLS prevention — all with one import.


Required Props — width and height (and Why They're Mandatory)

The most common question beginners ask is: "Why does next/image force me to specify width and height?"

The answer is Cumulative Layout Shift (CLS).

When the browser parses HTML, it lays out elements before images finish downloading. If it doesn't know an image's dimensions, it allocates zero space. When the image finally loads, everything below it jumps down — a layout shift that frustrates users and tanks your CLS score.

By requiring width and height, Next.js ensures the browser knows the aspect ratio before the image loads and can reserve the correct space.

import Image from 'next/image';

// These are the INTRINSIC dimensions of the source image
// They define the aspect ratio, NOT the rendered size on screen
// CSS still controls the actual display size
<Image
  src="/images/team-photo.jpg"
  alt="Our engineering team"
  width={800}   // Original image is 800px wide
  height={600}  // Original image is 600px tall
  // Aspect ratio: 4:3
  // The browser reserves a 4:3 space before the image loads
  // CSS can make this 400x300, 200x150, or 100% width — the ratio stays
/>

Key interview insight: width and height do NOT control the rendered size. They define the intrinsic dimensions so the browser can calculate the aspect ratio. You still use CSS to control how large the image appears on screen.

// Common pattern: width/height for ratio, className for display size
<Image
  src="/images/avatar.jpg"
  alt="User avatar"
  width={200}
  height={200}
  className="w-16 h-16 rounded-full"
  // width/height = 200x200 (1:1 ratio for CLS prevention)
  // className = renders at 64x64px with rounded corners
/>

Fill Mode — When You Don't Know the Dimensions

Sometimes you genuinely don't know the image dimensions ahead of time — user-uploaded avatars, CMS-managed hero images, or dynamically generated thumbnails. That's what the fill prop is for.

When you use fill, the image expands to fill its parent container. You don't pass width or height — the parent's dimensions control the space.

import Image from 'next/image';

// fill mode — image fills the parent container
export default function HeroBanner() {
  return (
    // CRITICAL: parent must have position: relative (or absolute/fixed)
    // and defined dimensions — otherwise the image has no boundaries
    <div className="relative w-full h-[400px]">
      <Image
        src="/images/hero-banner.jpg"
        alt="Summer sale banner"
        fill
        // No width/height needed — the parent div controls the space
        // The image fills the entire 100% x 400px area

        // object-fit controls how the image fills the space
        className="object-fit: cover"
        // 'cover' = fill the area, crop if needed (like background-size: cover)
        // 'contain' = fit inside the area, letterbox if needed
      />
    </div>
  );
}
// Common pattern: responsive card with fill image
export default function ProductCard({ product }: { product: Product }) {
  return (
    <div className="rounded-lg overflow-hidden shadow-lg">
      {/* Image container with fixed aspect ratio */}
      <div className="relative aspect-video">
        <Image
          src={product.imageUrl}
          alt={product.name}
          fill
          sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
          className="object-cover"
        />
      </div>
      <div className="p-4">
        <h3>{product.name}</h3>
        <p>${product.price}</p>
      </div>
    </div>
  );
}

Interview tip: If the interviewer asks "when would you use fill instead of width/height?", the answer is: when image dimensions are unknown at build time (user uploads, CMS content) or when you need the image to fill a CSS-defined container (hero sections, card thumbnails with aspect ratios).


The sizes Prop — Telling the Browser What It Needs

The sizes prop is the most misunderstood prop on next/image. Here is what it actually does:

When next/image generates a srcset (multiple image sizes), the browser needs to decide which size to download. But here is the problem — the browser picks the image before CSS loads. It can't check your stylesheet to see how wide the image will be rendered. So you have to tell it explicitly.

The sizes prop is a set of media query hints that tell the browser: "At this viewport width, this image will be rendered at this size."

import Image from 'next/image';

// Without sizes: browser assumes the image is 100vw (full viewport width)
// On a 1920px screen, it downloads the 1920px version
// But if the image only renders at 33% width, you wasted 66% of the download

// With sizes: browser knows exactly what it needs
<Image
  src="/images/product.jpg"
  alt="Product photo"
  width={800}
  height={800}
  sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
  // This tells the browser:
  // "On screens up to 640px, this image takes 100% of viewport width"
  // "On screens 641px to 1024px, this image takes 50% of viewport width"
  // "On screens wider than 1024px, this image takes 33% of viewport width"
  //
  // So on a 1920px desktop, the browser downloads the ~640px version (33% of 1920)
  // instead of the 1920px version. That's a MASSIVE bandwidth saving.
/>
// Another example: full-width hero on mobile, half-width on desktop
<Image
  src="/images/hero.jpg"
  alt="Hero image"
  fill
  sizes="(max-width: 768px) 100vw, 50vw"
  // Mobile (768px screen): downloads ~768px version
  // Desktop (1920px screen): downloads ~960px version (50% of 1920)
  // Without sizes: both would download the largest version
/>

Why it matters for interviews: sizes directly impacts Largest Contentful Paint (LCP). If a hero image downloads at 1920px when the rendered size is 500px, you're wasting bandwidth and slowing down LCP. The sizes prop is how you prevent that.


Priority — Boosting LCP Images

By default, next/image lazy-loads all images — they only start downloading when they're about to enter the viewport. This is excellent for off-screen images but terrible for the Largest Contentful Paint (LCP) element.

The LCP image is typically the hero image, the main product photo, or the first visible image. It needs to start downloading immediately, not when the user scrolls near it.

The priority prop disables lazy loading and adds a <link rel="preload"> to the document head, telling the browser to fetch this image with the highest priority.

import Image from 'next/image';

export default function HomePage() {
  return (
    <main>
      {/* This is the LCP image — it's the first and largest visible image */}
      <Image
        src="/images/hero-banner.jpg"
        alt="Welcome to our platform"
        width={1920}
        height={1080}
        priority
        // priority does two things:
        // 1. Removes loading="lazy" (image loads immediately)
        // 2. Adds <link rel="preload"> in <head> (browser fetches it ASAP)
        sizes="100vw"
      />

      {/* These images are below the fold — lazy loading is correct */}
      <Image
        src="/images/feature-1.jpg"
        alt="Feature one"
        width={600}
        height={400}
        // No priority = lazy loaded (default, correct for off-screen images)
      />
    </main>
  );
}

Rule of thumb: Only use priority on 1-2 images per page — the above-the-fold images that are visible on initial load. Using priority on every image defeats the purpose and makes everything compete for bandwidth.

In interviews, you should say: "I use priority on the LCP element, which is typically the hero image or the first meaningful image the user sees. This ensures it's preloaded and not blocked by lazy loading, directly improving Largest Contentful Paint scores."


Blur Placeholder — Perceived Performance

Even with priority, large images take time to download. A blank white rectangle while the image loads feels slow. Blur placeholders show a tiny, blurred preview of the image that transitions to the full image once it loads — creating a perception of speed.

import Image from 'next/image';

// For LOCAL (static) images: blur placeholder is automatic
import heroImage from '@/public/images/hero.jpg';

export default function HeroSection() {
  return (
    <Image
      src={heroImage}
      alt="Hero image"
      placeholder="blur"
      // Next.js automatically generates a tiny blurred version at build time
      // No additional configuration needed for static imports
      // The blurDataURL is embedded inline in the HTML (tiny base64 string)
    />
  );
}
// For REMOTE (dynamic) images: you must provide blurDataURL manually
// because Next.js can't generate it at build time for external URLs
<Image
  src="https://cdn.example.com/photos/sunset.jpg"
  alt="Sunset over the ocean"
  width={1200}
  height={800}
  placeholder="blur"
  blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD..."
  // You generate this base64 string ahead of time:
  // - Using a library like 'plaiceholder' at build time
  // - From your CMS (many CMS platforms provide blur hashes)
  // - Using the 'sharp' library in a server action or API route
/>
// Alternative: use the 'color' placeholder for a simpler approach
<Image
  src="https://cdn.example.com/photos/product.jpg"
  alt="Product photo"
  width={400}
  height={400}
  placeholder="empty"
  // 'empty' = no placeholder (default)
  // 'blur' = blurred preview
  // You can also use a solid color with CSS as a simpler alternative
  className="bg-gray-800"
  // Gray background shows while image loads — simpler than blur but still better
/>

Remote Images Configuration — next.config.js

By default, next/image only optimizes local images (from your /public folder). For remote images (from a CMS, CDN, or user uploads), you must explicitly allow the domains in next.config.js. This is a security measure — without it, anyone could use your server as a free image optimization proxy.

// next.config.js (or next.config.ts)

/** @type {import('next').NextConfig} */
const nextConfig = {
  images: {
    // Method 1: remotePatterns (RECOMMENDED — granular control)
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'cdn.example.com',
        port: '',
        pathname: '/images/**',
        // Only allows images from https://cdn.example.com/images/*
        // Blocks: https://cdn.example.com/admin/secret.jpg
      },
      {
        protocol: 'https',
        hostname: '*.amazonaws.com',
        // Wildcard: allows all S3 buckets
        // my-bucket.s3.amazonaws.com, other-bucket.s3.amazonaws.com
      },
      {
        protocol: 'https',
        hostname: 'images.unsplash.com',
        // Allow Unsplash images
      },
    ],

    // Method 2: domains (DEPRECATED in Next.js 14+ — use remotePatterns instead)
    // domains: ['cdn.example.com', 'images.unsplash.com'],
    // Less secure: allows ALL paths on the domain

    // Optional: customize device sizes for srcset generation
    deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
    // These are the viewport widths Next.js generates images for

    // Optional: customize image sizes for srcset generation
    imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
    // These are used for images with the sizes prop

    // Optional: set default format
    formats: ['image/avif', 'image/webp'],
    // Next.js will try AVIF first (best compression), then WebP, then original
  },
};

module.exports = nextConfig;
// After configuring remotePatterns, you can use remote URLs directly:
import Image from 'next/image';

export default function UserProfile({ user }: { user: User }) {
  return (
    <Image
      src={user.avatarUrl}
      // e.g., "https://cdn.example.com/images/avatars/user-123.jpg"
      alt={`${user.name}'s avatar`}
      width={120}
      height={120}
      className="rounded-full"
    />
  );
}

// If user.avatarUrl points to a domain NOT in remotePatterns,
// Next.js will throw an error at runtime:
// "Invalid src prop: hostname 'evil-site.com' is not configured under images in next.config.js"

Image Optimization visual 1


Common Mistakes

  • Using fill without a positioned parent. The fill prop makes the image position: absolute. If the parent doesn't have position: relative (or absolute/fixed), the image breaks out of its container and overlaps other elements. Always set the parent's position and give it explicit dimensions.

  • Forgetting sizes on fill images. When you use fill without a sizes prop, the browser assumes the image is 100vw wide — the entire viewport. On a desktop, this means downloading a 1920px image even if the container is only 300px wide. Always pair fill with a sizes prop that matches your layout.

  • Putting priority on every image. Marking all images as priority means nothing is prioritized. The browser downloads everything at once, competing for bandwidth. Only use priority on the 1-2 images that are above the fold and contribute to LCP.

  • Not configuring remotePatterns for external images. Developers use next/image with a remote URL, get an error, and switch back to <img> to "fix" it. The real fix is configuring remotePatterns in next.config.js. Never fall back to <img> just to avoid configuration.

  • Confusing width/height with display size. Setting width={1920} height={1080} doesn't make the image render at 1920x1080. These define the intrinsic dimensions for aspect ratio calculation. Use CSS (className, style) to control the actual rendered size.


Interview Questions

1. Why does the Next.js Image component require width and height props? What problem do they solve?

They prevent Cumulative Layout Shift (CLS). By knowing the image's dimensions before it loads, the browser can reserve the correct space in the layout. Without them, the page content jumps when the image loads. The width and height define the aspect ratio, not the rendered display size — CSS still controls how large the image appears. This is a Core Web Vital optimization that directly impacts user experience and SEO.

2. Explain the difference between using width/height and using the fill prop. When would you choose each?

Use width/height when you know the image's intrinsic dimensions at build time — local images, images with fixed aspect ratios, or icons. Use fill when dimensions are unknown or when the image needs to fill a CSS-defined container — user uploads, CMS content, hero banners that stretch full-width. With fill, the parent element must have position: relative and defined dimensions. Both approaches prevent CLS, just through different mechanisms.

3. What does the sizes prop do, and why is it important for performance?

The sizes prop tells the browser how wide the image will be rendered at different viewport widths. Since the browser picks which srcset image to download before CSS loads, it needs these hints to make the right choice. Without sizes, the browser assumes the image is 100vw wide and downloads the largest version. A proper sizes value like (max-width: 768px) 100vw, 33vw ensures mobile users download a small image and desktop users download a medium one — saving bandwidth and improving LCP.

4. When should you use the priority prop, and what happens under the hood when you set it?

Use priority on the Largest Contentful Paint (LCP) image — the first, most visually significant image visible without scrolling. Under the hood, priority removes loading="lazy" so the image starts downloading immediately, and it injects a <link rel="preload"> tag in the HTML <head> to tell the browser to fetch it with high priority. Use it on 1-2 images per page maximum. Using it on all images defeats the purpose and creates bandwidth contention.

5. A junior developer on your team is getting the error "hostname is not configured under images in next.config.js" when trying to use next/image with a remote URL. Explain the problem and the solution.

Next.js blocks remote image optimization by default as a security measure — otherwise, your server could be used as a free image optimization proxy for any URL on the internet. The fix is configuring remotePatterns in next.config.js with the specific protocol, hostname, and optionally pathname pattern of the allowed remote source. For example, { protocol: 'https', hostname: 'cdn.example.com', pathname: '/images/**' }. The deprecated domains array also works but offers less granular control. Never fall back to <img> just to avoid this configuration.


Quick Reference -- Cheat Sheet

ConceptKey Point
next/image solvesFormat conversion, responsive sizing, lazy loading, CLS, caching
width / heightDefine intrinsic dimensions (aspect ratio), NOT display size
fillImage fills parent container; parent needs position: relative
sizesTells browser how wide image renders at each viewport (critical for perf)
priorityPreloads LCP images; removes lazy loading; use on 1-2 images max
placeholder="blur"Shows blurred preview while loading; auto for static, manual for remote
blurDataURLBase64 string for remote image blur placeholders
Remote imagesMust configure remotePatterns in next.config.js
Format outputAuto-serves WebP/AVIF based on browser Accept header
Default quality75 (configurable via quality prop, range 1-100)
+-----------------------------------------------+
|         next/image Mental Model                |
+-----------------------------------------------+
|                                                |
|  1. Import Image from 'next/image'             |
|  2. Set src + alt (always)                     |
|  3. Set width + height OR fill                 |
|  4. Add sizes if image is not 100vw            |
|  5. Add priority if it's the LCP image         |
|  6. Add placeholder="blur" for perceived speed |
|  7. Configure remotePatterns for external URLs |
|                                                |
|  What you get automatically:                   |
|  + Lazy loading (off-screen images)            |
|  + WebP/AVIF format conversion                 |
|  + Responsive srcset generation                |
|  + CLS prevention (reserved space)             |
|  + On-demand optimization + caching            |
|                                                |
|  Decision tree:                                |
|  "Do I know the dimensions?"                   |
|    Yes -> width + height                       |
|    No  -> fill + parent with position:relative |
|                                                |
|  "Is this the first visible image?"            |
|    Yes -> priority                             |
|    No  -> default lazy loading                 |
|                                                |
+-----------------------------------------------+

Previous: Lesson 6.3 -- Authentication Patterns Next: Lesson 7.2 -- Font Optimization ->


This is Lesson 7.1 of the Next.js Interview Prep Course -- 8 chapters, 33 lessons.

On this page