EmpireUI
Get Pro
← Blog9 min read#next.js#image#optimisation

Next.js Image Optimisation: next/image Deep Dive — Every Prop Explained

Every prop in next/image explained with real code — from lazy loading and LCP priority to blur placeholders, remote patterns, and responsive sizes.

developer reviewing image optimisation code on a laptop screen

Why next/image Exists (and What You're Missing Without It)

The native <img> tag doesn't know or care about your users' viewport, connection speed, or whether they'll ever scroll down to see your image. It just fetches the full file. That's fine for a 2001 homepage, but in 2026 your Lighthouse scores disagree.

Next.js 13+ ships next/image as a first-class replacement that automatically converts images to WebP/AVIF, serves the right size for each device, lazy-loads off-screen images, and prevents cumulative layout shift (CLS) by reserving space in the DOM before the image loads. You get all of that for free — but only if you understand the props well enough to configure it correctly.

Honestly, most developers grab <Image src={img} width={800} height={600} alt="..." /> and call it a day. That works, but you're leaving significant performance on the table. The difference between a naively configured next/image and a properly tuned one can be 2–3 seconds off your LCP on a 4G connection — and LCP is a Core Web Vital that directly affects your search ranking.

This article walks through every meaningful prop, explains when you need it, and gives you copy-paste examples. No hand-waving, no "it depends" without an actual answer.

The Required Props: src, alt, width, and height

Three props are non-negotiable: src, alt, and either width+height or fill. Get any of these wrong and Next.js will throw at build time or, worse, silently produce broken layout.

src accepts a string path (relative to /public), an absolute URL, or an imported local image object — which is the cleanest option because the bundler reads the file's dimensions at build time:

import heroImg from '@/public/hero.png'; // dimensions auto-detected
import { Image } from 'next/image';

<Image
  src={heroImg}
  alt="Hero shot of the dashboard"
/>

When you import a local file this way, you can omit width and height entirely — Next.js fills them in. For remote URLs (a CMS, CDN, or user-uploaded content) you must supply explicit width and height. These values don't hard-code the rendered size — they define the aspect ratio the browser should reserve before the image loads, preventing the dreaded layout shift. Set them to the image's intrinsic pixel dimensions, then use CSS or Tailwind to actually control rendered size.

Worth noting: alt isn't just an accessibility requirement. Search crawlers use it to understand image content, and an empty alt="" tells them to ignore the image entirely. For decorative images that's correct. For content images — hero shots, product photos, blog covers — write something descriptive.

fill, sizes, and the Responsive Layout Pattern

The fill prop replaces width and height when you want the image to expand to fill its parent container. It's the go-to for hero banners, cover images, and any case where the intrinsic image dimensions are irrelevant because CSS controls the layout. The parent must have position: relative (or absolute/fixed).

<div className="relative w-full h-[480px]">
  <Image
    src="/hero.jpg"
    alt="Abstract gradient background"
    fill
    className="object-cover"
    priority
  />
</div>

Using fill without sizes is a common mistake. When Next.js doesn't know how wide the image will render, it falls back to generating a srcset based on the full viewport width — so mobile devices download a 1920px-wide image into a 375px slot. The sizes prop fixes this:

<Image
  src="/product.jpg"
  alt="Product detail view"
  fill
  sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
  className="object-cover rounded-xl"
/>

Read those sizes values like plain English: "on screens narrower than 768px I'm full width; between 768px and 1200px I'm half width; otherwise a third." Next.js uses this hint to pick the smallest adequate image from the generated srcset. On a 390px iPhone that means serving a ~400px-wide WebP instead of a 1200px one — roughly a 3× file-size reduction for free. That's the kind of win that moves your LCP by hundreds of milliseconds.

priority, loading, and LCP Tuning

By default, next/image lazy-loads everything. That's correct for images below the fold. It's catastrophic for your LCP hero image, which the browser now won't start fetching until the component mounts and the Intersection Observer fires — adding 300–600ms of delay on a fast connection, more on mobile.

Add priority to any image that's visible in the initial viewport without scrolling. It switches the image to eager loading and injects a <link rel="preload"> tag into the document <head>, so the browser discovers and starts fetching it as early as possible — before JavaScript has even run.

// The above-the-fold hero. Always add priority here.
<Image
  src="/hero.jpg"
  alt="Dashboard hero"
  width={1200}
  height={630}
  priority
/>

You should have at most one or two priority images per page. Marking everything as priority defeats the purpose — the browser has to fetch them all in parallel and none of them loads faster. In practice, one hero + one above-the-fold product image is the typical pattern.

The loading prop ("lazy" or "eager") is the lower-level toggle, but priority is the better choice for LCP images because it also adds the preload hint. Use loading="eager" only when you want eager fetching without the preload, which is a rare edge case in practice.

placeholder and blurDataURL — Loading States That Don't Suck

Blank white space while an image loads is visually jarring. placeholder="blur" fixes it by showing a blurred preview until the full image arrives. For local imported images, Next.js auto-generates the blur placeholder at build time — zero config needed.

import profilePic from '@/public/profile.jpg';

<Image
  src={profilePic}
  alt="Team member photo"
  width={80}
  height={80}
  placeholder="blur"
/>

For remote images you have to supply blurDataURL yourself because the build server can't reach an arbitrary CDN URL at compile time. The value is a tiny base64-encoded image — a 10×10px version of the original works well and is ~200 bytes:

// Generate a blur placeholder for remote images:
// You can create a tiny base64 JPEG with sharp or use a CDN's resize API
const blurUrl = 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD...';

<Image
  src="https://cdn.example.com/photo.jpg"
  alt="Remote photo"
  width={800}
  height={600}
  placeholder="blur"
  blurDataURL={blurUrl}
/>

Quick aside: services like Cloudinary and Imgix have resize URLs you can abuse for this. https://res.cloudinary.com/demo/image/upload/w_10,q_1/photo.jpg gives you a near-zero-byte thumbnail you can convert to base64 on the server and store alongside your CMS data. Set it up once and every image gets a native blur-up for free.

The placeholder="empty" default just shows nothing. For small avatars or icons that load fast, it's fine. For hero images or editorial photos that load over a few hundred milliseconds, reach for blur.

Remote Images: remotePatterns and next.config.js

Serving images from a CDN, Unsplash, a headless CMS, or any external host requires explicit allowlisting in next.config.js. Before Next.js 13.4, you used the domains array. That still works but it's deprecated — remotePatterns is more precise and should be your default now.

// next.config.js
/** @type {import('next').NextConfig} */
module.exports = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'images.unsplash.com',
        port: '',
        pathname: '/**',
      },
      {
        protocol: 'https',
        hostname: '**.cloudinary.com', // wildcard subdomain
      },
      {
        protocol: 'https',
        hostname: 'cdn.myapp.com',
        pathname: '/uploads/**', // restrict to a path prefix
      },
    ],
  },
};

The wildcard ** in hostname matches any number of subdomains, so **.cloudinary.com covers res.cloudinary.com, media.cloudinary.com, etc. The wildcard in pathname is simpler — /** matches any path. Lock this down as tightly as you can in production: accepting /** on a wildcard domain means any URL on that host can be proxied through your image endpoint.

Look, this matters for security, not just configuration hygiene. The /_next/image route acts as a reverse proxy that fetches and transforms images server-side. A too-permissive remotePatterns lets an attacker craft URLs that proxy arbitrary content through your infrastructure. If you're hosting on Vercel that's their problem, but if you're on a self-hosted Node.js server it's yours.

One more thing — if you need to disable the image optimisation entirely for a specific image (SVGs, for example, can't be meaningfully optimised and sometimes break), add unoptimized as a prop. Or set images: { unoptimized: true } in next.config.js to skip optimisation globally, which is sometimes useful for static exports.

Putting It All Together: A Production-Ready Image Component

Here's a wrapper component we reach for on most Empire UI projects. It handles LCP heroes, responsive fill layouts, and standard content images with one consistent API, and it plays nicely with the design tokens you'll find in our glassmorphism components or any of the other style hubs on Empire UI.

// components/OptimisedImage.tsx
import NextImage, { ImageProps } from 'next/image';

type OptimisedImageProps = ImageProps & {
  aspect?: string; // e.g. 'aspect-video', 'aspect-square'
};

export function OptimisedImage({
  src,
  alt,
  aspect,
  priority = false,
  sizes = '(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw',
  className = '',
  ...rest
}: OptimisedImageProps) {
  if (aspect) {
    // Fill mode — parent defines dimensions via aspect ratio class
    return (
      <div className={`relative w-full ${aspect}`}>
        <NextImage
          src={src}
          alt={alt}
          fill
          sizes={sizes}
          priority={priority}
          className={`object-cover ${className}`}
          {...rest}
        />
      </div>
    );
  }

  // Intrinsic-size mode — caller provides width + height
  return (
    <NextImage
      src={src}
      alt={alt}
      sizes={sizes}
      priority={priority}
      className={className}
      {...rest}
    />
  );
}

Usage is simple. Hero image: <OptimisedImage src="/hero.jpg" alt="..." aspect="aspect-video" priority />. Product thumbnail: <OptimisedImage src={product.img} alt={product.name} width={400} height={400} />. The sizes default covers the most common two-column-to-full-width responsive pattern and you can override it per call site.

The object-cover class on the fill variant is almost always what you want — it scales the image to cover the container while preserving aspect ratio, same as background-size: cover. Swap in object-contain for logos or product images where cropping would be destructive.

Test your results with Chrome DevTools > Network tab filtered to Img. Sort by size. If you're seeing any file over 200 KB on a mobile-width viewport, your sizes prop isn't doing its job. Real LCP targets for 2026: under 2.5s on a mid-tier Android device on 4G. next/image configured correctly gets you most of the way there without a separate image CDN.

FAQ

Do I need to specify width and height when using fill?

No. With fill, the image expands to its parent container, so width/height would be redundant. You do need the parent to have position: relative and defined dimensions via CSS or Tailwind classes.

What's the difference between priority and loading="eager"?

priority does everything loading="eager" does, plus it injects a <link rel="preload"> into the document head so the browser fetches the image before JS runs. Always use priority for above-the-fold images.

Can I use next/image with SVGs?

You can, but optimisation is skipped for SVGs by default since they're already vector-based. Add unoptimized to the component or serve SVGs as standard <img> tags — there's no quality benefit to running them through the image pipeline.

How do I generate a blurDataURL for remote images at scale?

Use your CDN's resize API to request a 10×10 version, then base64-encode it. Store it in your CMS alongside the main image URL. Cloudinary, Imgix, and Bunny all support sub-10px thumbnail generation via URL params.

Free components in 40 styles
React & Tailwind, copy-paste ready.
Browse →

Read next

Next.js Caching Deep Dive: Request, Data, Full Route and Client CacheNext.js Caching in 2026: fetch, ISR, Dynamic and No-Store ExplainedImage Optimisation in 2026: WebP, AVIF, Lazy Load and LCPWeb Font Loading in 2026: next/font, variable fonts and CLS