CCelestiallines.com
← Back to blog

Next.js Image Optimization Best Practices

By Pawan Singh · Jun 2026

Why the Built-In Image Component Exists at All

A plain <img> tag ships whatever size and format the source file happens to be, to every visitor, regardless of their device or viewport — a 3000px-wide photo gets sent in full even to a phone displaying it at 300px. next/image automatically resizes, converts format (typically to WebP/AVIF where the browser supports it), lazy-loads offscreen images, and prevents the layout shift that unstyled images commonly cause during load.

Basic Usage

import Image from 'next/image';

<Image
    src="/hero.jpg"
    alt="A descriptive, accurate alt text"
    width={1200}
    height={630}
/>

width and height aren't just display sizing — they tell Next.js the image's aspect ratio so it can reserve the correct space in the layout before the image finishes loading, which is what prevents Cumulative Layout Shift (a Core Web Vitals metric Google's ranking and UX scoring both weight directly).

The fill Prop for Unknown Dimensions

When an image needs to fill a container of a size determined by CSS rather than a fixed pixel size (a card grid, a responsive hero banner), use fill instead of explicit width/height:

<div style={{ position: 'relative', width: '100%', height: '400px' }}>
    <Image src="/hero.jpg" alt="..." fill style={{ objectFit: 'cover' }} />
</div>

The parent element must have position: relative (or similar) and an explicit size, since fill makes the image absolutely positioned to match its container exactly.

Prioritizing the Largest Above-the-Fold Image

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

priority disables lazy loading and tells the browser to fetch this image with high priority immediately — use it on whatever image is the page's Largest Contentful Paint candidate (almost always the largest visible image above the fold), and only that one. Marking every image on a page priority defeats the purpose entirely, since it removes the prioritization the flag exists to create.

Remote Images Need Explicit Configuration

Next.js refuses to optimize images from a domain it doesn't know about, for security reasons — allow specific remote hosts explicitly:

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

An image from an un-configured remote host will fail to optimize (and often fail to render at all) with a fairly clear error naming the missing configuration — this isn't a bug, it's a deliberate allowlist to prevent the optimizer being used as an open image proxy for arbitrary URLs.

The sizes Prop for Responsive Layouts

When an image's rendered width varies by viewport (full-width on mobile, half-width on desktop), sizes tells the browser which pre-generated size to actually download, rather than always fetching the largest variant "just in case":

<Image
    src="/hero.jpg"
    alt="..."
    fill
    sizes="(max-width: 768px) 100vw, 50vw"
/>

Omitting sizes on a fill image defaults to assuming 100% viewport width, which can mean over-fetching a larger image than the layout actually displays on wider screens where the image only occupies part of the viewport.

Local vs Remote Image Imports

Importing a local image file directly gives Next.js its dimensions automatically at build time, so width/height can be omitted:

import heroImage from '../public/hero.jpg';

<Image src={heroImage} alt="..." />

Remote images (from a CMS, a URL string) don't have this build-time information available, so width/height (or fill) must be provided explicitly — this is the most common reason a working local image example breaks when swapped for a dynamic, CMS-driven src.

Common Issues

  • "Invalid src prop... hostname is not configured" — the remote host is missing from remotePatterns in next.config.js.
  • Layout shift despite using next/image — missing or incorrect width/height (or a missing sized/positioned container when using fill), so the reserved space doesn't match the actual rendered size.
  • Largest Contentful Paint still slow — the LCP image isn't marked priority, so it's being lazy-loaded like any other offscreen image even though it's immediately visible.
  • Images look blurry/low quality — the source file itself is lower resolution than the size being requested; next/image can downscale but never upscale quality that isn't in the original.

Need a Next.js site's image performance actually optimized, not just using the component by default? Get in touch — I work as a freelance Next.js developer.

Comments