Migrate to RunxBuild and earn up to $50 in hosting credit on your first deposit.

Calculate your savings
unxBuild
Back to Blog Comparison

Next.js img vs Image: When the Component Is Worth It

Sean

Platform Writer

Aug 30, 2026
8 min read

next/image resizes, converts and lazy-loads images at request time. That optimisation runs on a server — so on a static export it does nothing, and the component becomes a plain img with extra configuration.

Next.js img vs Image: When the Component Is Worth It

The Next.js Image component is one of the framework’s genuinely valuable pieces. It solves layout shift, generates responsive sources, converts to modern formats and lazy-loads below-the-fold images, and you get all of that from an import.

It also has real constraints that are easy to miss until deployment, and there are cases where a plain <img> is the better answer.

Table of contents

What the component actually does

Given this:

import Image from 'next/image'

export default function Hero() {
  return (
    <Image
      src="/hero.jpg"
      alt="Aerial view of the harbour"
      width={1000}
      height={800}
      priority
    />
  )
}

You get rendered HTML with a generated srcset pointing at an optimisation endpoint, explicit dimensions, decoding="async", and loading="lazy" unless priority is set.

The four things it buys you:

  • Resizing at request time. A 4000px source is served at the size actually needed for the viewport.
  • Format conversion. WebP or AVIF where the browser accepts it, typically 25-50% smaller than JPEG.
  • No layout shift. Required width and height mean the browser reserves space before the image loads.
  • Lazy loading by default, with priority to opt out for above-the-fold images.

The layout shift point is the one with a measurable payoff. Cumulative Layout Shift is a Core Web Vitals metric, unsized images are the most common cause, and the component makes forgetting the dimensions a build error.

priority, and the mistake that costs you LCP

Every Image is lazy-loaded unless told otherwise. For an image below the fold that is correct. For your hero image it is a self-inflicted performance problem: the browser waits until layout to discover it is visible, then starts the request.

Your Largest Contentful Paint element — usually the hero — must have priority:

<Image src="/hero.jpg" alt="" width={1600} height={900} priority />

priority adds a preload hint and disables lazy loading. Use it only for images visible without scrolling, and typically only one or two per page — marking everything as priority preloads everything and is the same as prioritising nothing.

This single prop is the most common Next.js image mistake, and it is worth checking on any page whose LCP is disappointing.

Local, remote, and fill

Local images imported as modules give you dimensions automatically and a blur placeholder for free:

import hero from '@/public/hero.jpg'

<Image src={hero} alt="" placeholder="blur" />

This is the best form when the image is part of the build — Next reads the dimensions at build time, so you cannot get them wrong.

Remote images need explicit dimensions and an allowlist in the config, or Next refuses to optimise them:

// next.config.js
module.exports = {
  images: {
    remotePatterns: [
      { protocol: 'https', hostname: 'cdn.example.com', pathname: '/uploads/**' },
    ],
  },
}

The allowlist exists so that nobody can use your optimisation endpoint to resize arbitrary images from the internet at your expense. Keep it narrow — the older domains option is deprecated in favour of remotePatterns precisely because it was too coarse.

fill is for when the dimensions are unknown — a user upload, a CMS image — and lets the image size to its container:

<div style={{ position: 'relative', aspectRatio: '16/9' }}>
  <Image src={url} alt="" fill style={{ objectFit: 'cover' }} />
</div>

The parent must be positioned and must have its own dimensions, or the image collapses to nothing. That requirement catches everyone once.

The static export problem

This is the constraint that surprises people at deploy time. Image optimisation happens in a server route at request time. With output: 'export' there is no server, so the build fails unless you disable optimisation:

module.exports = {
  output: 'export',
  images: { unoptimized: true },
}

With unoptimized: true, the component renders a plain <img> pointing at the original file. You keep the layout-shift protection from the required dimensions and the lazy loading. You lose resizing and format conversion entirely — your 4MB source is served as 4MB.

So on a static export you have three options: optimise images before committing them and accept unoptimized, point the component at an external image CDN through a custom loader, or do not use static export.

The middle option is worth knowing about, since it decouples optimisation from your hosting:

// A custom loader lets any image service do the work
module.exports = {
  images: {
    loader: 'custom',
    loaderFile: './image-loader.js',
  },
}

When a plain img is the right call

The component is not always the answer:

  • SVGs. Next does not optimise them by default, and enabling SVG through the optimiser has security implications since SVG can carry script. Use <img>, or import the SVG as a component.
  • Tiny images. A 200-byte icon gains nothing from an optimisation round trip.
  • Images that must not be transformed. Anything where exact pixels matter — a QR code, a signature, a precise diagram.
  • Static exports with pre-optimised assets. If you already produce correctly-sized WebP at build time, the component is adding configuration for nothing.

When you do use <img>, do the two things the component would have done for you: set width and height to prevent layout shift, and set loading="lazy" on anything below the fold. Those are most of the benefit and cost nothing.

Where the optimisation runs

Worth being explicit about the cost model. On-demand optimisation means the first request for each size and format does real work — decode, resize, encode — and subsequent requests are served from cache. That is CPU on your server and bandwidth out of it, and both scale with traffic and with how many distinct sizes your sizes attribute generates.

A page with a wide range of breakpoints can generate a lot of variants. Constrain sizes to the widths you actually use rather than leaving it broad.

If the site is genuinely static, serving pre-optimised images from a static host is simpler and cheaper than running a server for the optimisation. On RunxBuild, static sites build from a repository with custom domains, headers, redirects and 120GB of bandwidth included before $0.10/GB, while a Next.js app that needs the runtime deploys as a service with a build log, a live route and rollback.

How this fits the rest of the stack

next/image earns its place through layout-shift prevention and automatic resizing, and the single highest-value thing you can do with it is put priority on the LCP image. Just be clear about where the optimisation runs — on a static export it does not, and a plain <img> with dimensions and lazy loading gets you most of the way. The RunxBuild hosting calculator shows the static-site path and the service path side by side so the bandwidth and compute are comparable.

Useful related references:

FAQ

Should I use next/image or a plain img tag?

Use next/image for content images that benefit from resizing and format conversion, and where enforced dimensions prevent layout shift. Use <img> for SVGs, tiny icons, images that must not be transformed, and static exports where you already ship optimised assets.

Why is my next/image not optimized?

Most often because images.unoptimized is true, which is required for output: 'export' since optimisation needs a server. It can also be a remote image whose hostname is not in remotePatterns, in which case Next refuses to process it.

What does the priority prop do?

It preloads the image and disables lazy loading. Set it on your Largest Contentful Paint element — usually the hero — because every Image is lazy-loaded by default, which delays the request until layout. Use it on only one or two images per page.

How do I use next/image with a static export?

Set images: { unoptimized: true } in next.config.js. The component then renders a plain <img>, keeping dimensions and lazy loading but losing resizing and format conversion, so optimise your source images before committing them.

Why does my image with fill not appear?

The parent element must be positioned — position: relative or similar — and must have its own dimensions or aspect ratio. With fill, the image sizes to its container, so a container with no height collapses the image to nothing.

#nextjs img#next/image#image optimization#Core Web Vitals#Next.js