Next.js Image Optimization: The 16 Upgrade Checklist

next/image is the biggest free performance win you get — but Next.js 16 deprecated priority, made qualities required, and dropped the cache default to 4 hours. Here's what to fix.

Images are the heaviest asset on most content pages and the most common Largest Contentful Paint (LCP) offender. next/image automates responsive sizing, modern formats, lazy loading, and CDN delivery for free. But Next.js 16 (default bundler is now Turbopack, as of June 2026 the current line is 16.2) changed three defaults that silently break sites upgrading from 14/15: priority is deprecated, qualities is now required in config, and the cache TTL dropped from a long default to 4 hours. This guide covers the working setup plus the exact breakages to fix.

TL;DR

  • Use preload instead of priority on your LCP image. priority is deprecated in Next.js 16.
  • Add qualities to next.config.js — it’s required as of Next.js 16, or the optimizer returns 400 Bad Request.
  • Set formats: ['image/avif', 'image/webp'] — AVIF is opt-in (default is WebP only) and compresses about 20% smaller.
  • Always set sizes so the browser fetches a width that matches your layout, not the largest in deviceSizes.
  • Bump minimumCacheTTL from the new 4-hour default (14400) to something longer if your images are stable.

What changed in Next.js 16

If your config and <Image> calls were written for Next.js 14 or 15, three things now behave differently (per the official version history, v16.0.0):

ItemNext.js 15Next.js 16 (as of June 2026)
LCP opt-in propprioritypreload (priority deprecated)
qualities configoptionalrequired; default [75]
minimumCacheTTL defaultlong-lived14400 (4 hours)
formats default['image/webp']['image/webp'] (AVIF still opt-in)
Disk cache capnonemaximumDiskCacheSize, defaults to 50% of free disk

The practical upshot: a config that worked last year may now throw on quality values or quietly re-fetch optimized images every four hours.

How to tell you have a problem

  • PageSpeed Insights flags “Properly size images” or “Serve images in next-gen formats.”
  • Your LCP element is a hero image and mobile LCP is over 2.5s (the “good” Core Web Vitals threshold).
  • The optimizer returns 400 Bad Request after upgrading — usually an unallowed quality value now that qualities is enforced.
  • next/image throws Invalid src prop, hostname is not configured on deploy.
  • You see Cumulative Layout Shift because images load after text.

Step by step

  1. Replace <img> with next/image. A static import gives you free width/height inference and an automatic blur placeholder:
import Image from 'next/image';
import heroImg from '@/public/hero.png';

export default function Page() {
  return (
    <Image
      src={heroImg}
      alt="Dashboard showing weekly metrics"
      placeholder="blur"
      sizes="(max-width: 768px) 100vw, 800px"
      preload    // LCP image only — see step 2
    />
  );
}
  1. LCP image gets preload, not priority. In Next.js 16 the priority prop is deprecated in favor of preload, which inserts a <link rel="preload"> in the <head>. Mark only one image per page — typically the hero, never a footer logo. The docs note: when the viewport changes which image is the LCP element, prefer fetchPriority="high" over preload so you don’t preload an image that’s hidden on some breakpoints.

  2. Configure the images block. This version sets the three things Next.js 16 cares about:

// next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
  images: {
    formats: ['image/avif', 'image/webp'],   // AVIF is opt-in; default is webp only
    qualities: [50, 75, 90],                  // required in Next.js 16 — quality prop must match
    deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
    imageSizes: [32, 48, 64, 96, 128, 256, 384],
    minimumCacheTTL: 2678400,                 // 31 days; default is now 14400 (4 hours)
    remotePatterns: [
      { protocol: 'https', hostname: 'images.unsplash.com', pathname: '/photos/**' },
      { protocol: 'https', hostname: 'cdn.yourdomain.com',  pathname: '/**' },
    ],
  },
};

export default nextConfig;

A quality prop that isn’t in your qualities array snaps to the nearest allowed value in the component, but a direct hit to the optimizer API with a disallowed quality returns 400. Keep the list short and intentional.

  1. Remote images need specific remotePatterns. Avoid bare ** for hostname. The Next.js 16 docs warn that omitting protocol, port, pathname, or search implies a wildcard, which lets attackers route arbitrary images through your optimizer. You can also use the shorthand new URL('https://example.com/account123/**'). For your own /public images, lock them down with localPatterns:
images: {
  localPatterns: [
    { pathname: '/assets/images/**', search: '' },
  ],
},
  1. sizes decides which width gets fetched. Without it, Next.js generates only a 1x/2x srcset; with it, you get the full responsive set (640w, 750w, …) and the browser picks the right one. Sample for a typical article hero (full width on mobile, ~800px on desktop):
<Image
  src={heroImg}
  alt="..."
  sizes="(max-width: 640px) 100vw,
         (max-width: 1024px) 80vw,
         800px"
  preload
/>
  1. fill needs a positioned parent (relative, absolute, or fixed), or the image collapses to zero height. Use it for unknown aspect ratios:
<div style={{ position: 'relative', aspectRatio: '16/9' }}>
  <Image src="/cover.jpg" alt="..." fill style={{ objectFit: 'cover' }} sizes="100vw" />
</div>
  1. Non-Vercel host? Pick a loader strategy. On Vercel the optimizer runs at the edge; elsewhere you run a Node server (request-time optimization), a custom loader, or pre-built assets. A custom loader hands optimization to Cloudflare Images, Imgix, or your own proxy:
const nextConfig = {
  images: {
    loader: 'custom',
    loaderFile: './lib/image-loader.ts',
  },
};
// lib/image-loader.ts
export default function loader({ src, width, quality }) {
  return `https://imgproxy.yourdomain.com/${width}/q${quality ?? 75}${src}`;
}

For a fully static export, you give up the built-in optimizer for portability:

const nextConfig = {
  output: 'export',
  images: { unoptimized: true },
};
  1. Verify with the DevTools Network tab. The hero should fire once with an image/avif content-type and a preload link in <head>. You can also check from the terminal:
curl -sI 'https://yourdomain.com/_next/image?url=/hero.png&w=1080&q=75' \
  | grep -iE 'content-type|cache-control'
# content-type: image/avif
# cache-control: public, max-age=2678400
  1. Measure the LCP impact. Lighthouse points at the actual largest paint element:
npx lighthouse https://yourdomain.com/articles/foo/ \
  --only-categories=performance --chrome-flags=--headless --quiet \
  | grep -A1 'largest-contentful-paint\|LCP element'

AVIF vs WebP: the real trade-off

The Next.js docs are blunt about it: AVIF takes about 50% longer to encode but compresses roughly 20% smaller than WebP. Two consequences for a content site:

  • The first request for an image is slower (the optimizer encodes on demand); every cached request after is faster and lighter.
  • Each format is cached separately, so enabling both AVIF and WebP roughly doubles your optimized-image storage. With the new maximumDiskCacheSize LRU eviction, that’s usually fine — but on a tiny VPS, set a cap or stick to WebP.

For photographic hero images on a blog, AVIF is worth it. For UI screenshots with flat color, the WebP-only default is often indistinguishable and encodes faster.

Common pitfalls

  • Still using priority after upgrading to Next.js 16. It works but is deprecated; migrate to preload.
  • Forgetting qualities in config, then getting 400 Bad Request from the optimizer on an unlisted quality value.
  • Leaving minimumCacheTTL at the new 4-hour default on a site with stable, content-hashed images — you re-optimize far more than needed.
  • Whitelisting ** for hostname in remotePatterns — every random host becomes an optimization target, raising cost and attack surface.
  • Using fill without a positioned parent — image collapses to zero height.
  • Omitting sizes and shipping a 2000px image to a 400px phone column.
  • Disabling optimization site-wide (unoptimized: true) to “fix” one bad image — you throw away the whole point of next/image.

Who this is for

Any Next.js site shipping images — blogs, marketing pages, product galleries, doc sites with diagrams — especially anyone who just upgraded to Next.js 16 and hasn’t audited their images config.

FAQ

  • Is priority removed in Next.js 16?: No, it’s deprecated, not removed — it still works but logs a warning. Replace it with preload for the LCP image, or fetchPriority="high" when the LCP element varies by viewport.
  • Why am I getting a 400 from the image optimizer after upgrading?: Next.js 16 made qualities required. If your <Image quality={...}> value isn’t in the allowed list (default [75]), a direct optimizer request returns 400. Add the values you use to images.qualities.
  • Does next/image work outside Vercel?: Yes. With a Node server it optimizes at request time; with a custom loader it offloads to Cloudflare Images or Imgix. A static export (output: 'export') needs an external loader or unoptimized: true.
  • Should I enable AVIF?: For photographic content, yes — it’s about 20% smaller than WebP. It encodes ~50% slower on the first request and doubles cache storage, so for flat UI screenshots the WebP default is often enough.
  • What about SVGs?: next/image won’t optimize SVG and disables it by default for security. If you must, set dangerouslyAllowSVG plus a strict contentSecurityPolicy; otherwise inline the SVG or use a plain <img>.

Tags: #Indie dev #Next.js #Core Web Vitals #Technical SEO #Getting started