← blog
NextjsOctober 13, 2026 · 15 min read

Next.js 16 Image: priority Deprecated, quality Now Allowlisted

Next.js 16 quietly deprecated Image's priority for preload and restricts quality to an allowlist (default: 75). What breaks, plus a copy-paste cheat sheet.

Parsa Jiravand · Frontend engineer · building bestpractic
Next.js 16 Image: priority Deprecated, quality Now Allowlisted

Your team ships a product page with a hero image: <Image src={photo} quality={90} priority fill />. It looked right in Next.js 15. You upgrade to 16, run a visual regression pass out of habit, and the hero image is... a little softer. Nothing crashed. No error in the terminal. No red line in CI. The quality={90} prop is still sitting right there in the JSX, doing nothing, and you'd have no way to know unless you happened to run the dev server and read a console warning that most people's eyes skip past.

That's the real story behind two changes Next.js 16 made to next/image: the quality prop is now checked against an allowlist (images.qualities, defaulting to [75]), and the priority prop is deprecated in favor of a new preload prop. Neither one is flashy enough to make a release-notes headline, and neither one throws a build error, which is exactly why they're worth a proper look: this article is written against Next.js 16.4 (verified against the framework's own next/image and next.config.js reference docs, and its source in packages/next/src/shared/lib, in October 2026), and by the end you'll know precisely what changed, why, and how to fix it on purpose instead of by accident.

By the end of this article you'll be able to:

  • Explain what images.qualities does, why it defaults to [75] in Next.js 16, and what happens to a quality value that isn't on the list
  • Predict whether a given quality prop gets silently coerced or causes a hard 400, and why those are two different code paths
  • Migrate priority to preload correctly, and understand why Next.js changed the name rather than the behavior
  • Read the Version History table at the bottom of a Next.js docs page and use it the way this article does
  • Configure images.qualities and preload/loading/fetchPriority together without fighting the framework's own validation

You've used next/image in an App Router project — at minimum, you've written <Image src={...} width={...} height={...} /> and set priority on a hero image at some point. You don't need prior knowledge of the Image Optimization API internals; we build the model from the prop down to the request.

Here's the Next.js 15 component from the intro, completely ordinary code:

TSX
1
2
3
4
5
6
7
8
// Works exactly as written on Next.js 15 <Image src={photo} alt="Studio monitor on a desk" fill quality={90} priority />

Upgrade to Next.js 16 with no other changes and two things happen, neither of which raises an error:

  • quality={90} stops meaning 90. Next.js 16 ships a default images.qualities: [75]. Since 90 isn't on that list, the component silently requests the closest allowed value instead — which, with the default config, is 75. In development you get a console warning. In a production build, there is no warning at all; the image is just a little softer than the designer approved.
  • priority is marked @deprecated in the type definitions, and the official docs now describe preload as the prop to reach for. Nothing breaks. Nothing tells you at runtime, either — unlike some other deprecated Image props (onLoadingComplete, for instance, logs a warning telling you to switch to onLoad), priority just keeps working, silently, for as long as you keep writing it.

Both changes share the same shape: a prop you set still "works," in the sense that your code compiles and the page renders, while quietly doing something other than what the Next.js 15-era mental model says it does. That's the gap this article closes.

It's tempting to think of <Image quality={90} priority /> as "pass these two numbers/flags to the <img> tag." It never was that simple, and in Next.js 16 it's explicit: both props go through a validation and normalization layer that consults your next.config.js before anything is requested over the network.

The mental model: <Image>'s props aren't a direct line to the browser. They're inputs to a decision Next.js makes on your behalf, and that decision depends on config you may not have touched. Quality gets snapped to the nearest value your config allows; priority-style loading urgency gets normalized into one signal. In both cases, the component decides what actually leaves your server — your prop is a request, not a guarantee.

Two separate decisions fall out of that model, and they're the two things that changed in 16:

  • What quality number actually gets requested — governed by images.qualities.
  • Whether the image loads eagerly and gets a <link rel="preload"> — governed by priority/preload/loading.

With no images config at all, Next.js 16's effective default is:

JavaScript
1
2
3
4
5
6
// next.config.js — this is the IMPLICIT default in Next.js 16, you don't have to write it module.exports = { images: { qualities: [75], }, }

Key concept: every <Image quality={N}> in your app is now checked against this array. If N isn't in it, Next.js doesn't throw — it picks whichever configured value is numerically closest to N and uses that instead.

This isn't a vague "rounds to a nice number" — it's a specific, small algorithm, and you can reason about it exactly:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
// This is Next.js's own logic (shared/lib/find-closest-quality.ts), reproduced for clarity function findClosestQuality(quality, configuredQualities) { const q = quality || 75 if (!configuredQualities?.length) return q return configuredQualities.reduce( (prev, cur) => (Math.abs(cur - q) < Math.abs(prev - q) ? cur : prev), configuredQualities[0] ) } findClosestQuality(90, [75]) // → 75 (only option) findClosestQuality(82, [50, 75, 100]) // → 75 (|82-75|=7 beats |82-100|=18) findClosestQuality(90, [50, 75, 100]) // → 100 (|90-100|=10 beats |90-75|=15)

Try the exact same function with your own numbers in the playground below — it runs this reduction live, not a simplified stand-in.

Runs right in your browser — poke at it and watch the concept react live.

If 75 is too aggressive for a photography-heavy site, widen the allowlist explicitly:

JavaScript
1
2
3
4
5
6
// next.config.js module.exports = { images: { qualities: [40, 60, 75, 90, 100], }, }

Now quality={90} on a component is requested as 90, exactly, because 90 is on the list. The fix for the regression in the intro isn't "add quality back" — it was never removed — it's "put 90 in images.qualities."

This is the part that trips people up, because there are genuinely two code paths, not one:

  • Through <Image> (the normal path): a disallowed quality is coerced to the closest allowed value. No error, no 4xx — your visitor gets a real image, just at a different quality than you asked for. Development logs a console warning naming the exact value to add to your config; production does not.
  • Through the raw optimization endpoint (GET /_next/image?url=...&w=...&q=90, the kind of URL you'd get by copying a src out of dev tools, or by hand-building a request): the server validates q against the same allowlist and, if it isn't an exact match, returns a 400 Bad Request. There's no "closest value" leniency here, because this endpoint isn't going through your component's props at all — it's a public HTTP surface, and Next.js treats an unlisted quality value the same way it treats a malformed width: a bad request.

So "what happens if the quality isn't allowed" has two honest answers depending on which door you walked through, and both are intentional: the component degrades gracefully for real visitors; the raw endpoint stays strict so the allowlist can't be bypassed by hitting the optimizer directly.

priority on <Image> has always done two things together: it stops the image from being treated as lazy-loaded (so the browser doesn't wait to discover it near the viewport), and it has Next.js insert a <link rel="preload"> for it, so the browser's preload scanner can start fetching it before React even renders the <img> tag. That combination is exactly what you want for a Largest Contentful Paint (LCP) candidate — your hero image, the above-the-fold product photo.

TSX
1
2
3
4
5
// Next.js 15 — still compiles on 16, still works, but the type is now @deprecated <Image src={hero} alt="…" priority fill /> // Next.js 16 — the documented way forward <Image src={hero} alt="…" preload fill />

Key concept: for the common case — "this is my LCP image, load it with urgency" — preload is a straight swap for priority. Both disable lazy-loading and both trigger the same preload <link> insertion; Next.js didn't change what the prop does here, it changed what it's called, specifically so the name describes the mechanism (a resource preload) instead of a vague notion of importance. You're not supposed to set both: the component throws "Only preload should be used" if a single <Image> has priority and preload together — but, like the quality-coercion warning, that check only runs while NODE_ENV !== 'production', so it surfaces in next dev, not in a next build/next start production run.

The docs are explicit that preload isn't always the right tool, even for an above-the-fold image: "In most cases, you should use loading="eager" or fetchPriority="high" instead of preload." Reach for those two directly when:

TSX
1
2
3
// Multiple images could plausibly be the LCP element depending on viewport size — // don't blanket them all with preload (that fights the browser's own prioritization) <Image src={hero} alt="…" loading="eager" fetchPriority="high" fill />

preload is a blunt, singular signal — "fetch this one early, with a <link> tag." loading/fetchPriority let the browser's own scheduler make the call across several candidates, which matters once a layout has more than one image that could end up being the LCP element on different screen sizes.

  • Nothing warns you to migrate priority. It's @deprecated in the types (your editor may gray it out or flag it with a linter that reads JSDoc), but there is no runtime console message pointing you to preload, so code that never gets a type-check pass can carry priority indefinitely without anyone noticing it's the old name.
  • The production quality warning doesn't exist. The images.qualities mismatch warning only fires in development (NODE_ENV !== 'production'). A staging environment running a production build will not tell you a quality prop is being coerced — only next dev will.
  • A static import's quality is checked too. quality behaves identically whether src is a remote URL, a path under public/, or a statically imported image — the allowlist check doesn't care how the src arrived.
  • Raising the allowlist raises optimizer work. Every quality value you add to images.qualities is a value the Image Optimization API may actually have to render on demand. A wide allowlist (say, every integer 1–100) defeats the point of having one at all — the docs frame the default narrow list as a deliberate guard against letting "malicious actors... optimize more qualities than you intended," since each distinct quality is a distinct image variant your server (or your hosting provider's optimizer) has to produce and cache.
  • priority/preload and loading="lazy" are mutually exclusive, and so are priority and preload together. The component throws for either combination rather than silently picking one — but that throw lives in the same NODE_ENV !== 'production'-gated code as the quality warning, so it's a next dev error, not a guarantee your next build/CI pipeline will catch it. If you only ever test with a production build, an invalid combination can ship unnoticed.

  • Set images.qualities once, deliberately, in next.config.js, based on the quality values your designs actually use — don't leave it at the 16 default if your app already standardizes on, say, 85 for photography.
  • Reserve preload for the one true LCP candidate on a given page. If more than one image could be it depending on viewport, use loading="eager"/fetchPriority="high" on the candidates instead and let the browser's scanner decide.
  • Run next dev at least once after a major upgrade, specifically to read the console for images.qualities warnings — they don't appear in a production build, and they're the only signal this change produces.
  • Grep for priority across .tsx files during a 15→16 upgrade and rename to preload as a mechanical pass; it's a safe, behavior-preserving rename for the common usage described above.
  • Don't widen images.qualities to "everything" as a reflexive fix — pick the two or three values your design system actually ships, the same way you'd pick deviceSizes.

No. If 90 isn't in your images.qualities allowlist, the component silently requests the closest allowed value instead (75, with the default config). No exception, no build failure — just a different number than the one in your JSX. The console warning for this only appears in development.

Because they're different code paths. The <Image> component coerces an out-of-range quality to the closest allowed value before building the request — it never sends an invalid q. The raw optimizer endpoint validates q as a strict allowlist with no coercion, because it's a public HTTP surface that has to reject bad input rather than guess what you meant.

No, it's deprecated, not removed. It still compiles and still behaves the same as preload for the eager-load-plus-preload-link use case. The Version History table in the official <Image> reference records it as deprecated starting in v16.0.0.

Not urgently — there's no runtime warning pushing you to, and the behavior hasn't changed. Treat it like any other mechanical rename: do it the next time you're already touching a file, or as a dedicated sweep during a major-version upgrade, rather than a hotfix.

Only partly. The dev-only console warning for an out-of-allowlist quality fires regardless of which loader you use. But the actual snap-to-closest-value coercion (findClosestQuality) is implemented inside Next.js's own default loader, not in a shared layer every loader passes through — a custom loader function or images.loaderFile receives the raw quality prop exactly as written and is responsible for applying (or ignoring) images.qualities itself. It also isn't going through the /_next/image endpoint in the typical case, so the strict-400 behavior doesn't apply there either.

JavaScript
1
2
3
4
5
6
// next.config.js — the Next.js 16 defaults, written out explicitly module.exports = { images: { qualities: [75], // REQUIRED allowlist since v16.0.0; unlisted quality → closest match }, }
TaskCodeWhat actually happens
Request a quality on the allowlist<Image quality={75} />Requested exactly as given
Request a quality NOT on the allowlist<Image quality={90} /> (allowlist [75])Silently coerced to 75 (closest match); dev-only console warning
Widen what's allowedimages: { qualities: [50, 75, 100] } quality={90} now resolves to 100 (closest), not 75
Hit the optimizer directly with a bad qualityGET /_next/image?url=...&w=640&q=90 (not on allowlist)400 Bad Request — no coercion on this path
Mark the LCP / hero image (current)<Image preload fill />Eager load + <link rel="preload"> inserted
Mark the LCP / hero image (deprecated, still works)<Image priority fill />Identical to preload — no runtime warning to switch
Multiple possible LCP candidates<Image loading="eager" fetchPriority="high" />Finer-grained signal than preload; lets the browser's scanner weigh several images
Invalid combination (next dev-only error)<Image priority preload /> or <Image priority loading="lazy" />Throws: "Only preload should be used" / "Only one should be used" — only while NODE_ENV !== 'production'

Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.

  • images.qualities defaults to [75] in Next.js 16. Any quality prop not on your configured allowlist is silently snapped to the closest value that is — gracefully through <Image>, strictly (400) through the raw optimizer endpoint.
  • The coercion warning is development-only. A production build will not tell you a quality value got rewritten; the only way to catch it is to run next dev and read the console, or to simply set the allowlist to match your design system up front.
  • priority isn't gone, it's renamed to preload, and for the one thing almost everyone uses it for — flagging the LCP image — the swap is behavior-preserving. Nothing nags you to make it, so plan it as a deliberate pass rather than waiting for a warning that will never come.
  • loading="eager" + fetchPriority="high" is the more precise tool when a page has more than one plausible LCP candidate; preload is for the single, known one.

You upgrade, the build succeeds, the page looks almost right — and that gap between "compiles" and "renders exactly as specified" is where both of these changes live. Now that you've seen the allowlist and the rename from the inside, a soft hero image or a silently-working priority prop stops being a mystery and starts being a one-line fix in next.config.js or a safe find-and-replace.

If you've been caught by either of these since upgrading to Next.js 16, or found a case the allowlist handles differently than described here, say so in the comments — these are the two next/image changes with the fewest search results and the most "wait, why did my image get softer" GitHub issues.

This episode builds on two earlier ones in this series: Next.js Cache Components Explained covers the broader caching model next/image sits alongside, and Next.js Route Handlers: GET Stopped Caching in 15 is the same "quiet default change with no error message" pattern applied to Route Handlers instead of <Image>.


🚀 Want more like this? Every guide, playground, and quiz lives on bestpractic.org — open it and sign up free so the next one finds you.

Thanks for reading! Let's stay connected:

Keep reading

One post a day, in your inbox

Each one with a runnable playground and a quiz. No pitch, no digest, unsubscribe in one click.

0 comments

Sign in to join the discussion, like comments, and save articles for later.