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.

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.qualitiesdoes, why it defaults to[75]in Next.js 16, and what happens to aqualityvalue that isn't on the list - Predict whether a given
qualityprop gets silently coerced or causes a hard 400, and why those are two different code paths - Migrate
prioritytopreloadcorrectly, 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.qualitiesandpreload/loading/fetchPrioritytogether 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.
- The problem: a prop that still compiles but silently does less
- The mental model: your props pass through a config-aware layer first
- Walkthrough: the quality allowlist
- Walkthrough: priority to preload
- Edge cases and gotchas
- Best practices
- FAQ
- Cheat sheet
- Key takeaways
Here's the Next.js 15 component from the intro, completely ordinary code:
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 defaultimages.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.priorityis marked@deprecatedin the type definitions, and the official docs now describepreloadas the prop to reach for. Nothing breaks. Nothing tells you at runtime, either — unlike some other deprecatedImageprops (onLoadingComplete, for instance, logs a warning telling you to switch toonLoad),priorityjust 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 bypriority/preload/loading.
With no images config at all, Next.js 16's effective default is:
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:
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:
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 disallowedqualityis 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 asrcout of dev tools, or by hand-building a request): the server validatesqagainst 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.
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:
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@deprecatedin 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 topreload, so code that never gets a type-check pass can carrypriorityindefinitely without anyone noticing it's the old name. - The production quality warning doesn't exist. The
images.qualitiesmismatch warning only fires in development (NODE_ENV !== 'production'). A staging environment running a production build will not tell you aqualityprop is being coerced — onlynext devwill. - A static import's
qualityis checked too.qualitybehaves identically whethersrcis a remote URL, a path underpublic/, or a statically imported image — the allowlist check doesn't care how thesrcarrived. - Raising the allowlist raises optimizer work. Every quality value you add to
images.qualitiesis 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/preloadandloading="lazy"are mutually exclusive, and so arepriorityandpreloadtogether. The component throws for either combination rather than silently picking one — but that throw lives in the sameNODE_ENV !== 'production'-gated code as the quality warning, so it's anext deverror, not a guarantee yournext build/CI pipeline will catch it. If you only ever test with a production build, an invalid combination can ship unnoticed.
- Set
images.qualitiesonce, deliberately, innext.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
preloadfor the one true LCP candidate on a given page. If more than one image could be it depending on viewport, useloading="eager"/fetchPriority="high"on the candidates instead and let the browser's scanner decide. - Run
next devat least once after a major upgrade, specifically to read the console forimages.qualitieswarnings — they don't appear in a production build, and they're the only signal this change produces. - Grep for
priorityacross.tsxfiles during a 15→16 upgrade and rename topreloadas a mechanical pass; it's a safe, behavior-preserving rename for the common usage described above. - Don't widen
images.qualitiesto "everything" as a reflexive fix — pick the two or three values your design system actually ships, the same way you'd pickdeviceSizes.
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.
| Task | Code | What 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 allowed | images: { qualities: [50, 75, 100] } | quality={90} now resolves to 100 (closest), not 75 |
| Hit the optimizer directly with a bad quality | GET /_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' |
Think it clicked? Take the 8-question quiz →
Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.
images.qualitiesdefaults to[75]in Next.js 16. Anyqualityprop 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 devand read the console, or to simply set the allowlist to match your design system up front. priorityisn't gone, it's renamed topreload, 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;preloadis 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>.
- Next.js Cache Components Explained (with Cheat Sheet)
- Next.js Parallel & Intercepting Routes: Modals Done Right
- Next.js Server Actions: Mutations & Security (Cheat Sheet)
🚀 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:
- ⭐ GitHub — follow me and star the projects: github.com/parsajiravand
- 💬 Discord — join the frontend best-practices community: discord.gg/d9KRhuAwQ
- 📸 Instagram — frontend best practices, daily: @bestpractice___
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.