[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"quiz-nextjs-weekly-image-priority-quality-changes":44,"search-suggestions":60,"quiz-article-nextjs-weekly-image-priority-quality-changes":108},[4,20,32],{"id":5,"slug":6,"name":7,"tagline":8,"description":9,"accentFrom":10,"accentTo":11,"icon":12,"defaultLocale":13,"locales":14,"features":16,"position":19},"019fe637-3d33-714b-b57f-23e163ffca0c","dev","Web Development","Read it. Run it. Prove it.","A post a day on modern web development — most with an editable playground and a quiz that explains every answer. Free, no account needed.","violet-500","cyan-400","◇","en",[13,15],"fa",{"courses":17,"paths":17,"articles":17,"exams":18,"flashcards":18,"packages":17,"community":17,"certificates":17,"teams":17,"commerce":17},true,false,0,{"id":21,"slug":22,"name":23,"tagline":24,"description":25,"accentFrom":26,"accentTo":10,"icon":27,"defaultLocale":13,"locales":28,"features":30,"position":31},"019fe637-3dc2-754c-8657-0f175bfee7c6","lang","Languages","Learn a language the way you learn a codebase.","Grammar explained the way good documentation explains an API — one idea at a time, each with a quiz.","amber-400","⌘",[13,15,29],"es",{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":17,"packages":18,"community":17,"certificates":17,"teams":18,"commerce":18},2,{"id":33,"slug":34,"name":35,"tagline":36,"description":37,"accentFrom":38,"accentTo":39,"icon":40,"defaultLocale":13,"locales":41,"features":42,"position":43},"7b3c16f2-931d-410e-802e-e1fa4edab7de","soft","Soft Skills","The half of the job nobody wrote documentation for.","Weekly, on the parts of working life that decide more than your code does — first weeks, meetings, interviews, promotions, and the people around you. Written from what actually happens, and recorded as a podcast you can listen to on the walk.","emerald-400","teal-300","◉",[13],{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":18,"packages":18,"community":17,"certificates":18,"teams":18,"commerce":18},3,{"id":45,"slug":46,"kind":47,"title":48,"description":49,"config":50,"verticalId":5,"vertical":55,"course":52,"_count":56,"access":57,"attempts":59,"questionCount":51},"01a120b9-d664-7293-ba45-a5fecd81ec36","nextjs-weekly-image-priority-quality-changes","PRACTICE_QUIZ","Next.js 16 Image: priority, preload, and the quality allowlist","Test yourself on the two Next.js 16 changes to next\u002Fimage: the images.qualities allowlist and the priority-to-preload rename. Checked against the Next.js 16.4 docs and source.",{"questionCount":51,"timeLimitSec":52,"shuffleQuestions":18,"shuffleOptions":17,"negativeMarking":19,"passScorePct":53,"maxAttempts":52,"revealAnswers":54,"allowFlagging":18,"allowBacktracking":17},8,null,70,"AFTER_SUBMIT",{"slug":6,"name":7},{"questions":51},{"allowed":17,"reason":58},"FREE",[],[61,65,69,73,77,81,85,89,93,97,101,105],{"slug":62,"name":63,"articles":64},"webdev","Webdev",137,{"slug":66,"name":67,"articles":68},"javascript","Javascript",112,{"slug":70,"name":71,"articles":72},"frontend","Frontend",83,{"slug":74,"name":75,"articles":76},"tutorial","Tutorial",53,{"slug":78,"name":79,"articles":80},"css","Css",42,{"slug":82,"name":83,"articles":84},"performance","Performance",20,{"slug":86,"name":87,"articles":88},"typescript","Typescript",18,{"slug":90,"name":91,"articles":92},"react","React",17,{"slug":94,"name":95,"articles":96},"browser","Browser",13,{"slug":98,"name":99,"articles":100},"node","Node",12,{"slug":102,"name":103,"articles":104},"programming","Programming",9,{"slug":106,"name":107,"articles":104},"accessibility","Accessibility",{"id":109,"slug":46,"title":110,"subtitle":52,"excerpt":111,"coverUrl":112,"locale":13,"readingMinutes":113,"publishedAt":114,"viewCount":115,"likeCount":19,"commentCount":19,"author":116,"vertical":121,"topic":122,"tags":125,"_count":130,"playground":132,"body":134,"bodyMd":500,"seo":501,"translationGroupId":503,"series":504,"podcastUrl":52,"verticalId":5,"thread":534,"assessments":536,"translations":539,"quiz":541},"01a120b9-d611-74ef-ad15-a267677c3251","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.","\u002Fmedia\u002Fcovers\u002Fnextjs-weekly-image-priority-quality-changes.png",15,"2026-10-13T06:10:29.815Z",32,{"id":117,"name":118,"username":119,"avatarUrl":52,"headline":120},"019fe637-3c25-7088-9034-39c9f15dc3c8","Parsa Jiravand","parsa","Frontend engineer · building bestpractic",{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":123,"name":124},"nextjs","Nextjs",[126,127,128,129],{"slug":123,"name":124,"color":52},{"slug":90,"name":91,"color":52},{"slug":62,"name":63,"color":52},{"slug":74,"name":75,"color":52},{"assessments":131},1,{"slug":46,"title":133},"Next.js 16 Image pipeline — interactive playground",{"blocks":135,"version":131},[136,140,143,148,151,160,163,166,169,181,184,187,193,196,201,204,207,210,213,216,221,224,227,230,235,238,242,245,249,252,255,258,261,264,267,270,273,277,280,283,286,291,294,297,301,304,308,312,315,319,322,326,329,332,340,343,351,354,358,361,365,368,372,375,379,382,386,389,392,396,436,439,442,445,448,451,454,461,464,467,470,473,476,482,485,488,491,494],{"id":137,"html":138,"type":139},"b1","\u003Cp>Your team ships a product page with a hero image: \u003Ccode>&lt;Image src={photo} quality={90} priority fill \u002F&gt;\u003C\u002Fcode>. 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 \u003Ccode>quality={90}\u003C\u002Fcode> prop is still sitting right there in the JSX, doing nothing, and you&#39;d have no way to know unless you happened to run the dev server and read a console warning that most people&#39;s eyes skip past.\u003C\u002Fp>","paragraph",{"id":141,"html":142,"type":139},"b2","\u003Cp>That&#39;s the real story behind two changes Next.js 16 made to \u003Ccode>next\u002Fimage\u003C\u002Fcode>: the \u003Ccode>quality\u003C\u002Fcode> prop is now checked against an allowlist (\u003Ccode>images.qualities\u003C\u002Fcode>, defaulting to \u003Ccode>[75]\u003C\u002Fcode>), and the \u003Ccode>priority\u003C\u002Fcode> prop is deprecated in favor of a new \u003Ccode>preload\u003C\u002Fcode> prop. Neither one is flashy enough to make a release-notes headline, and neither one throws a build error, which is exactly why they&#39;re worth a proper look: this article is written against \u003Cstrong>Next.js 16.4\u003C\u002Fstrong> (verified against the framework&#39;s own \u003Ccode>next\u002Fimage\u003C\u002Fcode> and \u003Ccode>next.config.js\u003C\u002Fcode> reference docs, and its source in \u003Ccode>packages\u002Fnext\u002Fsrc\u002Fshared\u002Flib\u003C\u002Fcode>, in October 2026), and by the end you&#39;ll know precisely what changed, why, and how to fix it on purpose instead of by accident.\u003C\u002Fp>",{"id":144,"html":145,"text":146,"type":147,"level":31},"b3","What you&#39;ll learn","What you'll learn","heading",{"id":149,"html":150,"type":139},"b4","\u003Cp>By the end of this article you&#39;ll be able to:\u003C\u002Fp>",{"id":152,"type":153,"items":154,"ordered":18},"b5","list",[155,156,157,158,159],"Explain what \u003Ccode>images.qualities\u003C\u002Fcode> does, why it defaults to \u003Ccode>[75]\u003C\u002Fcode> in Next.js 16, and what happens to a \u003Ccode>quality\u003C\u002Fcode> value that isn&#39;t on the list","Predict whether a given \u003Ccode>quality\u003C\u002Fcode> prop gets silently coerced or causes a hard 400, and why those are two different code paths","Migrate \u003Ccode>priority\u003C\u002Fcode> to \u003Ccode>preload\u003C\u002Fcode> 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 \u003Ccode>images.qualities\u003C\u002Fcode> and \u003Ccode>preload\u003C\u002Fcode>\u002F\u003Ccode>loading\u003C\u002Fcode>\u002F\u003Ccode>fetchPriority\u003C\u002Fcode> together without fighting the framework&#39;s own validation",{"id":161,"html":162,"text":162,"type":147,"level":31},"b6","Who this is for",{"id":164,"html":165,"type":139},"b7","\u003Cp>You&#39;ve used \u003Ccode>next\u002Fimage\u003C\u002Fcode> in an App Router project — at minimum, you&#39;ve written \u003Ccode>&lt;Image src={...} width={...} height={...} \u002F&gt;\u003C\u002Fcode> and set \u003Ccode>priority\u003C\u002Fcode> on a hero image at some point. You don&#39;t need prior knowledge of the Image Optimization API internals; we build the model from the prop down to the request.\u003C\u002Fp>",{"id":167,"html":168,"text":168,"type":147,"level":31},"b8","Table of contents",{"id":170,"type":153,"items":171,"ordered":18},"b9",[172,173,174,175,176,177,178,179,180],"\u003Ca href=\"#the-problem-a-prop-that-still-compiles-but-silently-does-less\">The problem: a prop that still compiles but silently does less\u003C\u002Fa>","\u003Ca href=\"#the-mental-model-your-props-pass-through-a-config-aware-layer-first\">The mental model: your props pass through a config-aware layer first\u003C\u002Fa>","\u003Ca href=\"#walkthrough-the-quality-allowlist\">Walkthrough: the quality allowlist\u003C\u002Fa>","\u003Ca href=\"#walkthrough-priority-to-preload\">Walkthrough: priority to preload\u003C\u002Fa>","\u003Ca href=\"#edge-cases-and-gotchas\">Edge cases and gotchas\u003C\u002Fa>","\u003Ca href=\"#best-practices\">Best practices\u003C\u002Fa>","\u003Ca href=\"#faq\">FAQ\u003C\u002Fa>","\u003Ca href=\"#cheat-sheet\">Cheat sheet\u003C\u002Fa>","\u003Ca href=\"#key-takeaways\">Key takeaways\u003C\u002Fa>",{"id":182,"html":183,"text":183,"type":147,"level":31},"b10","The problem: a prop that still compiles but silently does less",{"id":185,"html":186,"type":139},"b11","\u003Cp>Here&#39;s the Next.js 15 component from the intro, completely ordinary code:\u003C\u002Fp>",{"id":188,"code":189,"type":190,"language":191,"highlight":192},"b12","\u002F\u002F Works exactly as written on Next.js 15\n\u003CImage\n  src={photo}\n  alt=\"Studio monitor on a desk\"\n  fill\n  quality={90}\n  priority\n\u002F>","code","tsx",[],{"id":194,"html":195,"type":139},"b13","\u003Cp>Upgrade to Next.js 16 with no other changes and two things happen, neither of which raises an error:\u003C\u002Fp>",{"id":197,"type":153,"items":198,"ordered":17},"b14",[199,200],"\u003Cstrong>\u003Ccode>quality={90}\u003C\u002Fcode> stops meaning 90.\u003C\u002Fstrong> Next.js 16 ships a default \u003Ccode>images.qualities: [75]\u003C\u002Fcode>. Since 90 isn&#39;t on that list, the component silently requests the \u003Cem>closest allowed value\u003C\u002Fem> 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.","\u003Cstrong>\u003Ccode>priority\u003C\u002Fcode> is marked \u003Ccode>@deprecated\u003C\u002Fcode> in the type definitions\u003C\u002Fstrong>, and the official docs now describe \u003Ccode>preload\u003C\u002Fcode> as the prop to reach for. Nothing breaks. Nothing tells you at runtime, either — unlike some other deprecated \u003Ccode>Image\u003C\u002Fcode> props (\u003Ccode>onLoadingComplete\u003C\u002Fcode>, for instance, logs a warning telling you to switch to \u003Ccode>onLoad\u003C\u002Fcode>), \u003Ccode>priority\u003C\u002Fcode> just keeps working, silently, for as long as you keep writing it.",{"id":202,"html":203,"type":139},"b15","\u003Cp>Both changes share the same shape: a prop you set still &quot;works,&quot; 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&#39;s the gap this article closes.\u003C\u002Fp>",{"id":205,"html":206,"text":206,"type":147,"level":31},"b16","The mental model: your props pass through a config-aware layer first",{"id":208,"html":209,"type":139},"b17","\u003Cp>It&#39;s tempting to think of \u003Ccode>&lt;Image quality={90} priority \u002F&gt;\u003C\u002Fcode> as &quot;pass these two numbers\u002Fflags to the \u003Ccode>&lt;img&gt;\u003C\u002Fcode> tag.&quot; It never was that simple, and in Next.js 16 it&#39;s explicit: both props go through a validation and normalization layer that consults your \u003Ccode>next.config.js\u003C\u002Fcode> before anything is requested over the network.\u003C\u002Fp>",{"id":211,"html":212,"type":139},"b18","\u003Cp>\u003Cstrong>The mental model:\u003C\u002Fstrong> \u003Ccode>&lt;Image&gt;\u003C\u002Fcode>&#39;s props aren&#39;t a direct line to the browser. They&#39;re \u003Cem>inputs to a decision Next.js makes on your behalf\u003C\u002Fem>, and that decision depends on config you may not have touched. Quality gets \u003Cstrong>snapped\u003C\u002Fstrong> to the nearest value your config allows; priority-style loading urgency gets \u003Cstrong>normalized\u003C\u002Fstrong> into one signal. In both cases, the component decides what actually leaves your server — your prop is a request, not a guarantee.\u003C\u002Fp>",{"id":214,"html":215,"type":139},"b19","\u003Cp>Two separate decisions fall out of that model, and they&#39;re the two things that changed in 16:\u003C\u002Fp>",{"id":217,"type":153,"items":218,"ordered":18},"b20",[219,220],"\u003Cstrong>What quality number actually gets requested\u003C\u002Fstrong> — governed by \u003Ccode>images.qualities\u003C\u002Fcode>.","\u003Cstrong>Whether the image loads eagerly and gets a \u003Ccode>&lt;link rel=&quot;preload&quot;&gt;\u003C\u002Fcode>\u003C\u002Fstrong> — governed by \u003Ccode>priority\u003C\u002Fcode>\u002F\u003Ccode>preload\u003C\u002Fcode>\u002F\u003Ccode>loading\u003C\u002Fcode>.",{"id":222,"html":223,"text":223,"type":147,"level":31},"b21","Walkthrough: the quality allowlist",{"id":225,"html":226,"text":226,"type":147,"level":43},"b22","Stage 1 — the default, unconfigured app",{"id":228,"html":229,"type":139},"b23","\u003Cp>With no \u003Ccode>images\u003C\u002Fcode> config at all, Next.js 16&#39;s effective default is:\u003C\u002Fp>",{"id":231,"code":232,"type":190,"language":233,"highlight":234},"b24","\u002F\u002F next.config.js — this is the IMPLICIT default in Next.js 16, you don't have to write it\nmodule.exports = {\n  images: {\n    qualities: [75],\n  },\n}","js",[],{"id":236,"html":237,"type":139},"b25","\u003Cp>\u003Cstrong>Key concept:\u003C\u002Fstrong> every \u003Ccode>&lt;Image quality={N}&gt;\u003C\u002Fcode> in your app is now checked against this array. If \u003Ccode>N\u003C\u002Fcode> isn&#39;t in it, Next.js doesn&#39;t throw — it picks whichever configured value is numerically closest to \u003Ccode>N\u003C\u002Fcode> and uses that instead.\u003C\u002Fp>",{"id":239,"html":240,"text":241,"type":147,"level":43},"b26","Stage 2 — what &quot;closest&quot; actually means","Stage 2 — what \"closest\" actually means",{"id":243,"html":244,"type":139},"b27","\u003Cp>This isn&#39;t a vague &quot;rounds to a nice number&quot; — it&#39;s a specific, small algorithm, and you can reason about it exactly:\u003C\u002Fp>",{"id":246,"code":247,"type":190,"language":233,"highlight":248},"b28","\u002F\u002F This is Next.js's own logic (shared\u002Flib\u002Ffind-closest-quality.ts), reproduced for clarity\nfunction findClosestQuality(quality, configuredQualities) {\n  const q = quality || 75\n  if (!configuredQualities?.length) return q\n  return configuredQualities.reduce(\n    (prev, cur) => (Math.abs(cur - q) \u003C Math.abs(prev - q) ? cur : prev),\n    configuredQualities[0]\n  )\n}\n\nfindClosestQuality(90, [75])           \u002F\u002F → 75 (only option)\nfindClosestQuality(82, [50, 75, 100])  \u002F\u002F → 75 (|82-75|=7 beats |82-100|=18)\nfindClosestQuality(90, [50, 75, 100])  \u002F\u002F → 100 (|90-100|=10 beats |90-75|=15)",[],{"id":250,"html":251,"type":139},"b29","\u003Cp>Try the exact same function with your own numbers in the playground below — it runs this reduction live, not a simplified stand-in.\u003C\u002Fp>",{"id":253,"html":254,"type":139},"b30","\u003C!-- playground:start -->",{"id":256,"html":257,"text":257,"type":147,"level":31},"b31","🎮 Try it yourself",{"id":259,"html":260,"type":139},"b32","\u003Cp>\u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-image-priority-quality-changes\u002Fplayground\">▶️ Open the interactive playground →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":262,"html":263,"type":139},"b33","\u003Cp>\u003Cem>Runs right in your browser — poke at it and watch the concept react live.\u003C\u002Fem>\u003C\u002Fp>",{"id":265,"html":266,"type":139},"b34","\u003C!-- playground:end -->",{"id":268,"html":269,"text":269,"type":147,"level":43},"b35","Stage 3 — configuring it on purpose",{"id":271,"html":272,"type":139},"b36","\u003Cp>If 75 is too aggressive for a photography-heavy site, widen the allowlist explicitly:\u003C\u002Fp>",{"id":274,"code":275,"type":190,"language":233,"highlight":276},"b37","\u002F\u002F next.config.js\nmodule.exports = {\n  images: {\n    qualities: [40, 60, 75, 90, 100],\n  },\n}",[],{"id":278,"html":279,"type":139},"b38","\u003Cp>Now \u003Ccode>quality={90}\u003C\u002Fcode> on a component is requested as 90, exactly, because 90 is on the list. The fix for the regression in the intro isn&#39;t &quot;add quality back&quot; — it was never removed — it&#39;s &quot;put 90 in \u003Ccode>images.qualities\u003C\u002Fcode>.&quot;\u003C\u002Fp>",{"id":281,"html":282,"text":282,"type":147,"level":43},"b39","Stage 4 — the two different failure modes",{"id":284,"html":285,"type":139},"b40","\u003Cp>This is the part that trips people up, because there are genuinely two code paths, not one:\u003C\u002Fp>",{"id":287,"type":153,"items":288,"ordered":18},"b41",[289,290],"\u003Cstrong>Through \u003Ccode>&lt;Image&gt;\u003C\u002Fcode> (the normal path):\u003C\u002Fstrong> a disallowed \u003Ccode>quality\u003C\u002Fcode> 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.","\u003Cstrong>Through the raw optimization endpoint\u003C\u002Fstrong> (\u003Ccode>GET \u002F_next\u002Fimage?url=...&amp;w=...&amp;q=90\u003C\u002Fcode>, the kind of URL you&#39;d get by copying a \u003Ccode>src\u003C\u002Fcode> out of dev tools, or by hand-building a request): the server validates \u003Ccode>q\u003C\u002Fcode> against the same allowlist and, if it isn&#39;t an exact match, returns a \u003Cstrong>400 Bad Request\u003C\u002Fstrong>. There&#39;s no &quot;closest value&quot; leniency here, because this endpoint isn&#39;t going through your component&#39;s props at all — it&#39;s a public HTTP surface, and Next.js treats an unlisted quality value the same way it treats a malformed width: a bad request.",{"id":292,"html":293,"type":139},"b42","\u003Cp>So &quot;what happens if the quality isn&#39;t allowed&quot; 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&#39;t be bypassed by hitting the optimizer directly.\u003C\u002Fp>",{"id":295,"html":296,"text":296,"type":147,"level":31},"b43","Walkthrough: priority to preload",{"id":298,"html":299,"text":300,"type":147,"level":43},"b44","Stage 1 — what \u003Ccode>priority\u003C\u002Fcode> always did","Stage 1 — what priority always did",{"id":302,"html":303,"type":139},"b45","\u003Cp>\u003Ccode>priority\u003C\u002Fcode> on \u003Ccode>&lt;Image&gt;\u003C\u002Fcode> has always done two things together: it stops the image from being treated as lazy-loaded (so the browser doesn&#39;t wait to discover it near the viewport), and it has Next.js insert a \u003Ccode>&lt;link rel=&quot;preload&quot;&gt;\u003C\u002Fcode> for it, so the browser&#39;s preload scanner can start fetching it before React even renders the \u003Ccode>&lt;img&gt;\u003C\u002Fcode> tag. That combination is exactly what you want for a Largest Contentful Paint (LCP) candidate — your hero image, the above-the-fold product photo.\u003C\u002Fp>",{"id":305,"html":306,"text":307,"type":147,"level":43},"b46","Stage 2 — the rename, and why it&#39;s a rename and not a behavior change","Stage 2 — the rename, and why it's a rename and not a behavior change",{"id":309,"code":310,"type":190,"language":191,"highlight":311},"b47","\u002F\u002F Next.js 15 — still compiles on 16, still works, but the type is now @deprecated\n\u003CImage src={hero} alt=\"…\" priority fill \u002F>\n\n\u002F\u002F Next.js 16 — the documented way forward\n\u003CImage src={hero} alt=\"…\" preload fill \u002F>",[],{"id":313,"html":314,"type":139},"b48","\u003Cp>\u003Cstrong>Key concept:\u003C\u002Fstrong> for the common case — &quot;this is my LCP image, load it with urgency&quot; — \u003Ccode>preload\u003C\u002Fcode> is a straight swap for \u003Ccode>priority\u003C\u002Fcode>. Both disable lazy-loading and both trigger the same preload \u003Ccode>&lt;link&gt;\u003C\u002Fcode> insertion; Next.js didn&#39;t change what the prop \u003Cem>does\u003C\u002Fem> here, it changed what it&#39;s \u003Cem>called\u003C\u002Fem>, specifically so the name describes the mechanism (a resource preload) instead of a vague notion of importance. You&#39;re not supposed to set both: the component throws &quot;Only \u003Ccode>preload\u003C\u002Fcode> should be used&quot; if a single \u003Ccode>&lt;Image&gt;\u003C\u002Fcode> has \u003Ccode>priority\u003C\u002Fcode> and \u003Ccode>preload\u003C\u002Fcode> together — but, like the quality-coercion warning, that check only runs while \u003Ccode>NODE_ENV !== &#39;production&#39;\u003C\u002Fcode>, so it surfaces in \u003Ccode>next dev\u003C\u002Fcode>, not in a \u003Ccode>next build\u003C\u002Fcode>\u002F\u003Ccode>next start\u003C\u002Fcode> production run.\u003C\u002Fp>",{"id":316,"html":317,"text":318,"type":147,"level":43},"b49","Stage 3 — reaching past \u003Ccode>preload\u003C\u002Fcode> when you need more control","Stage 3 — reaching past preload when you need more control",{"id":320,"html":321,"type":139},"b50","\u003Cp>The docs are explicit that \u003Ccode>preload\u003C\u002Fcode> isn&#39;t always the right tool, even for an above-the-fold image: &quot;In most cases, you should use \u003Ccode>loading=&quot;eager&quot;\u003C\u002Fcode> or \u003Ccode>fetchPriority=&quot;high&quot;\u003C\u002Fcode> instead of \u003Ccode>preload\u003C\u002Fcode>.&quot; Reach for those two directly when:\u003C\u002Fp>",{"id":323,"code":324,"type":190,"language":191,"highlight":325},"b51","\u002F\u002F Multiple images could plausibly be the LCP element depending on viewport size —\n\u002F\u002F don't blanket them all with preload (that fights the browser's own prioritization)\n\u003CImage src={hero} alt=\"…\" loading=\"eager\" fetchPriority=\"high\" fill \u002F>",[],{"id":327,"html":328,"type":139},"b52","\u003Cp>\u003Ccode>preload\u003C\u002Fcode> is a blunt, singular signal — &quot;fetch this one early, with a \u003Ccode>&lt;link&gt;\u003C\u002Fcode> tag.&quot; \u003Ccode>loading\u003C\u002Fcode>\u002F\u003Ccode>fetchPriority\u003C\u002Fcode> let the browser&#39;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.\u003C\u002Fp>",{"id":330,"html":331,"text":331,"type":147,"level":31},"b53","Edge cases and gotchas",{"id":333,"type":153,"items":334,"ordered":18},"b54",[335,336,337,338,339],"\u003Cstrong>Nothing warns you to migrate \u003Ccode>priority\u003C\u002Fcode>.\u003C\u002Fstrong> It&#39;s \u003Ccode>@deprecated\u003C\u002Fcode> 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 \u003Ccode>preload\u003C\u002Fcode>, so code that never gets a type-check pass can carry \u003Ccode>priority\u003C\u002Fcode> indefinitely without anyone noticing it&#39;s the old name.","\u003Cstrong>The production quality warning doesn&#39;t exist.\u003C\u002Fstrong> The \u003Ccode>images.qualities\u003C\u002Fcode> mismatch warning only fires in development (\u003Ccode>NODE_ENV !== &#39;production&#39;\u003C\u002Fcode>). A staging environment running a production build will not tell you a \u003Ccode>quality\u003C\u002Fcode> prop is being coerced — only \u003Ccode>next dev\u003C\u002Fcode> will.","\u003Cstrong>A static import&#39;s \u003Ccode>quality\u003C\u002Fcode> is checked too.\u003C\u002Fstrong> \u003Ccode>quality\u003C\u002Fcode> behaves identically whether \u003Ccode>src\u003C\u002Fcode> is a remote URL, a path under \u003Ccode>public\u002F\u003C\u002Fcode>, or a statically imported image — the allowlist check doesn&#39;t care how the \u003Ccode>src\u003C\u002Fcode> arrived.","\u003Cstrong>Raising the allowlist raises optimizer work.\u003C\u002Fstrong> Every quality value you add to \u003Ccode>images.qualities\u003C\u002Fcode> 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 &quot;malicious actors... optimize more qualities than you intended,&quot; since each distinct quality is a distinct image variant your server (or your hosting provider&#39;s optimizer) has to produce and cache.","\u003Cstrong>\u003Ccode>priority\u003C\u002Fcode>\u002F\u003Ccode>preload\u003C\u002Fcode> and \u003Ccode>loading=&quot;lazy&quot;\u003C\u002Fcode> are mutually exclusive, and so are \u003Ccode>priority\u003C\u002Fcode> and \u003Ccode>preload\u003C\u002Fcode> together.\u003C\u002Fstrong> The component throws for either combination rather than silently picking one — but that throw lives in the same \u003Ccode>NODE_ENV !== &#39;production&#39;\u003C\u002Fcode>-gated code as the quality warning, so it&#39;s a \u003Ccode>next dev\u003C\u002Fcode> error, not a guarantee your \u003Ccode>next build\u003C\u002Fcode>\u002FCI pipeline will catch it. If you only ever test with a production build, an invalid combination can ship unnoticed.",{"id":341,"html":342,"text":342,"type":147,"level":31},"b55","Best practices",{"id":344,"type":153,"items":345,"ordered":18},"b56",[346,347,348,349,350],"\u003Cstrong>Set \u003Ccode>images.qualities\u003C\u002Fcode> once, deliberately, in \u003Ccode>next.config.js\u003C\u002Fcode>\u003C\u002Fstrong>, based on the quality values your designs actually use — don&#39;t leave it at the 16 default if your app already standardizes on, say, 85 for photography.","\u003Cstrong>Reserve \u003Ccode>preload\u003C\u002Fcode> for the one true LCP candidate\u003C\u002Fstrong> on a given page. If more than one image could be it depending on viewport, use \u003Ccode>loading=&quot;eager&quot;\u003C\u002Fcode>\u002F\u003Ccode>fetchPriority=&quot;high&quot;\u003C\u002Fcode> on the candidates instead and let the browser&#39;s scanner decide.","\u003Cstrong>Run \u003Ccode>next dev\u003C\u002Fcode> at least once after a major upgrade\u003C\u002Fstrong>, specifically to read the console for \u003Ccode>images.qualities\u003C\u002Fcode> warnings — they don&#39;t appear in a production build, and they&#39;re the only signal this change produces.","\u003Cstrong>Grep for \u003Ccode>priority\u003C\u002Fcode> across \u003Ccode>.tsx\u003C\u002Fcode> files during a 15→16 upgrade\u003C\u002Fstrong> and rename to \u003Ccode>preload\u003C\u002Fcode> as a mechanical pass; it&#39;s a safe, behavior-preserving rename for the common usage described above.","\u003Cstrong>Don&#39;t widen \u003Ccode>images.qualities\u003C\u002Fcode> to &quot;everything&quot;\u003C\u002Fstrong> as a reflexive fix — pick the two or three values your design system actually ships, the same way you&#39;d pick \u003Ccode>deviceSizes\u003C\u002Fcode>.",{"id":352,"html":353,"text":353,"type":147,"level":31},"b57","FAQ",{"id":355,"html":356,"text":357,"type":147,"level":43},"b58","Does \u003Ccode>quality={90}\u003C\u002Fcode> throw an error in Next.js 16?","Does quality={90} throw an error in Next.js 16?",{"id":359,"html":360,"type":139},"b59","\u003Cp>No. If 90 isn&#39;t in your \u003Ccode>images.qualities\u003C\u002Fcode> 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.\u003C\u002Fp>",{"id":362,"html":363,"text":364,"type":147,"level":43},"b60","Why does hitting \u003Ccode>\u002F_next\u002Fimage?...&amp;q=90\u003C\u002Fcode> directly return a 400 but the component doesn&#39;t error?","Why does hitting \u002Fnext\u002Fimage?...&q=90 directly return a 400 but the component doesn't error?",{"id":366,"html":367,"type":139},"b61","\u003Cp>Because they&#39;re different code paths. The \u003Ccode>&lt;Image&gt;\u003C\u002Fcode> component coerces an out-of-range quality to the closest allowed value before building the request — it never sends an invalid \u003Ccode>q\u003C\u002Fcode>. The raw optimizer endpoint validates \u003Ccode>q\u003C\u002Fcode> as a strict allowlist with no coercion, because it&#39;s a public HTTP surface that has to reject bad input rather than guess what you meant.\u003C\u002Fp>",{"id":369,"html":370,"text":371,"type":147,"level":43},"b62","Is \u003Ccode>priority\u003C\u002Fcode> removed in Next.js 16?","Is priority removed in Next.js 16?",{"id":373,"html":374,"type":139},"b63","\u003Cp>No, it&#39;s deprecated, not removed. It still compiles and still behaves the same as \u003Ccode>preload\u003C\u002Fcode> for the eager-load-plus-preload-link use case. The Version History table in the official \u003Ccode>&lt;Image&gt;\u003C\u002Fcode> reference records it as deprecated starting in \u003Ccode>v16.0.0\u003C\u002Fcode>.\u003C\u002Fp>",{"id":376,"html":377,"text":378,"type":147,"level":43},"b64","Do I need to migrate \u003Ccode>priority\u003C\u002Fcode> to \u003Ccode>preload\u003C\u002Fcode> immediately?","Do I need to migrate priority to preload immediately?",{"id":380,"html":381,"type":139},"b65","\u003Cp>Not urgently — there&#39;s no runtime warning pushing you to, and the behavior hasn&#39;t changed. Treat it like any other mechanical rename: do it the next time you&#39;re already touching a file, or as a dedicated sweep during a major-version upgrade, rather than a hotfix.\u003C\u002Fp>",{"id":383,"html":384,"text":385,"type":147,"level":43},"b66","Does \u003Ccode>images.qualities\u003C\u002Fcode> affect images served through a custom loader?","Does images.qualities affect images served through a custom loader?",{"id":387,"html":388,"type":139},"b67","\u003Cp>Only partly. The dev-only console warning for an out-of-allowlist \u003Ccode>quality\u003C\u002Fcode> fires regardless of which loader you use. But the actual snap-to-closest-value coercion (\u003Ccode>findClosestQuality\u003C\u002Fcode>) is implemented inside Next.js&#39;s own \u003Cstrong>default\u003C\u002Fstrong> loader, not in a shared layer every loader passes through — a custom \u003Ccode>loader\u003C\u002Fcode> function or \u003Ccode>images.loaderFile\u003C\u002Fcode> receives the raw \u003Ccode>quality\u003C\u002Fcode> prop exactly as written and is responsible for applying (or ignoring) \u003Ccode>images.qualities\u003C\u002Fcode> itself. It also isn&#39;t going through the \u003Ccode>\u002F_next\u002Fimage\u003C\u002Fcode> endpoint in the typical case, so the strict-400 behavior doesn&#39;t apply there either.\u003C\u002Fp>",{"id":390,"html":391,"text":391,"type":147,"level":31},"b68","Cheat sheet",{"id":393,"code":394,"type":190,"language":233,"highlight":395},"b69","\u002F\u002F next.config.js — the Next.js 16 defaults, written out explicitly\nmodule.exports = {\n  images: {\n    qualities: [75],        \u002F\u002F REQUIRED allowlist since v16.0.0; unlisted quality → closest match\n  },\n}",[],{"id":397,"head":398,"rows":402,"type":435},"b70",[399,400,401],"Task","Code","What actually happens",[403,407,411,415,419,423,427,431],[404,405,406],"Request a quality on the allowlist","\u003Ccode>&lt;Image quality={75} \u002F&gt;\u003C\u002Fcode>","Requested exactly as given",[408,409,410],"Request a quality NOT on the allowlist","\u003Ccode>&lt;Image quality={90} \u002F&gt;\u003C\u002Fcode> (allowlist \u003Ccode>[75]\u003C\u002Fcode>)","Silently coerced to \u003Ccode>75\u003C\u002Fcode> (closest match); dev-only console warning",[412,413,414],"Widen what&#39;s allowed","\u003Ccode>images: { qualities: [50, 75, 100] } \u003C\u002Fcode>","\u003Ccode>quality={90}\u003C\u002Fcode> now resolves to \u003Ccode>100\u003C\u002Fcode> (closest), not \u003Ccode>75\u003C\u002Fcode>",[416,417,418],"Hit the optimizer directly with a bad quality","\u003Ccode>GET \u002F_next\u002Fimage?url=...&amp;w=640&amp;q=90\u003C\u002Fcode> (not on allowlist)","\u003Cstrong>400 Bad Request\u003C\u002Fstrong> — no coercion on this path",[420,421,422],"Mark the LCP \u002F hero image (current)","\u003Ccode>&lt;Image preload fill \u002F&gt;\u003C\u002Fcode>","Eager load + \u003Ccode>&lt;link rel=&quot;preload&quot;&gt;\u003C\u002Fcode> inserted",[424,425,426],"Mark the LCP \u002F hero image (deprecated, still works)","\u003Ccode>&lt;Image priority fill \u002F&gt;\u003C\u002Fcode>","Identical to \u003Ccode>preload\u003C\u002Fcode> — no runtime warning to switch",[428,429,430],"Multiple possible LCP candidates","\u003Ccode>&lt;Image loading=&quot;eager&quot; fetchPriority=&quot;high&quot; \u002F&gt;\u003C\u002Fcode>","Finer-grained signal than \u003Ccode>preload\u003C\u002Fcode>; lets the browser&#39;s scanner weigh several images",[432,433,434],"Invalid combination (\u003Ccode>next dev\u003C\u002Fcode>-only error)","\u003Ccode>&lt;Image priority preload \u002F&gt;\u003C\u002Fcode> or \u003Ccode>&lt;Image priority loading=&quot;lazy&quot; \u002F&gt;\u003C\u002Fcode>","Throws: &quot;Only \u003Ccode>preload\u003C\u002Fcode> should be used&quot; \u002F &quot;Only one should be used&quot; — only while \u003Ccode>NODE_ENV !== &#39;production&#39;\u003C\u002Fcode>","table",{"id":437,"html":438,"type":139},"b71","\u003C!-- quiz:start -->",{"id":440,"html":441,"text":441,"type":147,"level":31},"b72","🧠 Test yourself",{"id":443,"html":444,"type":139},"b73","\u003Cp>Think it clicked? \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-image-priority-quality-changes\u002Fquiz\">Take the 8-question quiz →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":446,"html":447,"type":139},"b74","\u003Cp>\u003Cem>Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.\u003C\u002Fem>\u003C\u002Fp>",{"id":449,"html":450,"type":139},"b75","\u003C!-- quiz:end -->",{"id":452,"html":453,"text":453,"type":147,"level":31},"b76","Key takeaways",{"id":455,"type":153,"items":456,"ordered":18},"b77",[457,458,459,460],"\u003Cstrong>\u003Ccode>images.qualities\u003C\u002Fcode> defaults to \u003Ccode>[75]\u003C\u002Fcode> in Next.js 16.\u003C\u002Fstrong> Any \u003Ccode>quality\u003C\u002Fcode> prop not on your configured allowlist is silently snapped to the closest value that is — gracefully through \u003Ccode>&lt;Image&gt;\u003C\u002Fcode>, strictly (400) through the raw optimizer endpoint.","\u003Cstrong>The coercion warning is development-only.\u003C\u002Fstrong> A production build will not tell you a quality value got rewritten; the only way to catch it is to run \u003Ccode>next dev\u003C\u002Fcode> and read the console, or to simply set the allowlist to match your design system up front.","\u003Cstrong>\u003Ccode>priority\u003C\u002Fcode> isn&#39;t gone, it&#39;s renamed to \u003Ccode>preload\u003C\u002Fcode>\u003C\u002Fstrong>, 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.","\u003Cstrong>\u003Ccode>loading=&quot;eager&quot;\u003C\u002Fcode> + \u003Ccode>fetchPriority=&quot;high&quot;\u003C\u002Fcode> is the more precise tool\u003C\u002Fstrong> when a page has more than one plausible LCP candidate; \u003Ccode>preload\u003C\u002Fcode> is for the single, known one.",{"id":462,"html":463,"type":139},"b78","\u003Cp>You upgrade, the build succeeds, the page looks \u003Cem>almost\u003C\u002Fem> right — and that gap between &quot;compiles&quot; and &quot;renders exactly as specified&quot; is where both of these changes live. Now that you&#39;ve seen the allowlist and the rename from the inside, a soft hero image or a silently-working \u003Ccode>priority\u003C\u002Fcode> prop stops being a mystery and starts being a one-line fix in \u003Ccode>next.config.js\u003C\u002Fcode> or a safe find-and-replace.\u003C\u002Fp>",{"id":465,"html":466,"type":139},"b79","\u003Cp>If you&#39;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\u002Fimage changes with the fewest search results and the most &quot;wait, why did my image get softer&quot; GitHub issues.\u003C\u002Fp>",{"id":468,"html":469,"type":139},"b80","\u003Cp>This episode builds on two earlier ones in this series: \u003Ca href=\"https:\u002F\u002Fdev.to\u002Fparsajiravand\u002Fnextjs-cache-components-explained-with-cheat-sheet-55ob\">Next.js Cache Components Explained\u003C\u002Fa> covers the broader caching model \u003Ccode>next\u002Fimage\u003C\u002Fcode> sits alongside, and \u003Ca href=\"https:\u002F\u002Fdev.to\u002Fparsajiravand\u002Fnextjs-route-handlers-get-stopped-caching-in-15-how-to-cache-in-16-56nb\">Next.js Route Handlers: GET Stopped Caching in 15\u003C\u002Fa> is the same &quot;quiet default change with no error message&quot; pattern applied to Route Handlers instead of \u003Ccode>&lt;Image&gt;\u003C\u002Fcode>.\u003C\u002Fp>",{"id":471,"html":472,"type":139},"b81","\u003C!-- related:start -->",{"id":474,"html":475,"text":475,"type":147,"level":31},"b82","📚 Read next",{"id":477,"type":153,"items":478,"ordered":18},"b83",[479,480,481],"\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-cache-components-explained\">Next.js Cache Components Explained (with Cheat Sheet)\u003C\u002Fa>","\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-parallel-intercepting-routes-modals\">Next.js Parallel &amp; Intercepting Routes: Modals Done Right\u003C\u002Fa>","\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-server-actions-mutations-security\">Next.js Server Actions: Mutations &amp; Security (Cheat Sheet)\u003C\u002Fa>",{"id":483,"html":484,"type":139},"b84","\u003C!-- related:end -->",{"id":486,"type":487},"b85","divider",{"id":489,"html":490,"type":139},"b86","\u003Cp>🚀 \u003Cstrong>Want more like this?\u003C\u002Fstrong> Every guide, playground, and quiz lives on \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002F\">bestpractic.org\u003C\u002Fa>\u003C\u002Fstrong> — open it and \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002F\">sign up free\u003C\u002Fa>\u003C\u002Fstrong> so the next one finds you.\u003C\u002Fp>",{"id":492,"html":493,"type":139},"b87","\u003Cp>\u003Cem>Thanks for reading! Let&#39;s stay connected:\u003C\u002Fem>\u003C\u002Fp>",{"id":495,"type":153,"items":496,"ordered":18},"b88",[497,498,499],"⭐ \u003Cstrong>GitHub\u003C\u002Fstrong> — follow me and star the projects: \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fparsajiravand\">github.com\u002Fparsajiravand\u003C\u002Fa>","💬 \u003Cstrong>Discord\u003C\u002Fstrong> — join the frontend best-practices community: \u003Ca href=\"https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ\">discord.gg\u002Fd9KRhuAwQ\u003C\u002Fa>","📸 \u003Cstrong>Instagram\u003C\u002Fstrong> — frontend best practices, daily: \u003Ca href=\"https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F\">@bestpractice___\u003C\u002Fa>","Your team ships a product page with a hero image: `\u003CImage src={photo} quality={90} priority fill \u002F>`. 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.\n\nThat's the real story behind two changes Next.js 16 made to `next\u002Fimage`: 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\u002Fimage` and `next.config.js` reference docs, and its source in `packages\u002Fnext\u002Fsrc\u002Fshared\u002Flib`, 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.\n\n## What you'll learn\n\nBy the end of this article you'll be able to:\n\n- 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\n- Predict whether a given `quality` prop gets silently coerced or causes a hard 400, and why those are two different code paths\n- Migrate `priority` to `preload` correctly, and understand why Next.js changed the name rather than the behavior\n- Read the Version History table at the bottom of a Next.js docs page and use it the way this article does\n- Configure `images.qualities` and `preload`\u002F`loading`\u002F`fetchPriority` together without fighting the framework's own validation\n\n## Who this is for\n\nYou've used `next\u002Fimage` in an App Router project — at minimum, you've written `\u003CImage src={...} width={...} height={...} \u002F>` 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.\n\n## Table of contents\n\n- [The problem: a prop that still compiles but silently does less](#the-problem-a-prop-that-still-compiles-but-silently-does-less)\n- [The mental model: your props pass through a config-aware layer first](#the-mental-model-your-props-pass-through-a-config-aware-layer-first)\n- [Walkthrough: the quality allowlist](#walkthrough-the-quality-allowlist)\n- [Walkthrough: priority to preload](#walkthrough-priority-to-preload)\n- [Edge cases and gotchas](#edge-cases-and-gotchas)\n- [Best practices](#best-practices)\n- [FAQ](#faq)\n- [Cheat sheet](#cheat-sheet)\n- [Key takeaways](#key-takeaways)\n\n## The problem: a prop that still compiles but silently does less\n\nHere's the Next.js 15 component from the intro, completely ordinary code:\n\n```tsx\n\u002F\u002F Works exactly as written on Next.js 15\n\u003CImage\n  src={photo}\n  alt=\"Studio monitor on a desk\"\n  fill\n  quality={90}\n  priority\n\u002F>\n```\n\nUpgrade to Next.js 16 with no other changes and two things happen, neither of which raises an error:\n\n1. **`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.\n2. **`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.\n\nBoth 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.\n\n## The mental model: your props pass through a config-aware layer first\n\nIt's tempting to think of `\u003CImage quality={90} priority \u002F>` as \"pass these two numbers\u002Fflags to the `\u003Cimg>` 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.\n\n**The mental model:** `\u003CImage>`'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.\n\nTwo separate decisions fall out of that model, and they're the two things that changed in 16:\n\n- **What quality number actually gets requested** — governed by `images.qualities`.\n- **Whether the image loads eagerly and gets a `\u003Clink rel=\"preload\">`** — governed by `priority`\u002F`preload`\u002F`loading`.\n\n## Walkthrough: the quality allowlist\n\n### Stage 1 — the default, unconfigured app\n\nWith no `images` config at all, Next.js 16's effective default is:\n\n```js\n\u002F\u002F next.config.js — this is the IMPLICIT default in Next.js 16, you don't have to write it\nmodule.exports = {\n  images: {\n    qualities: [75],\n  },\n}\n```\n\n**Key concept:** every `\u003CImage 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.\n\n### Stage 2 — what \"closest\" actually means\n\nThis isn't a vague \"rounds to a nice number\" — it's a specific, small algorithm, and you can reason about it exactly:\n\n```js\n\u002F\u002F This is Next.js's own logic (shared\u002Flib\u002Ffind-closest-quality.ts), reproduced for clarity\nfunction findClosestQuality(quality, configuredQualities) {\n  const q = quality || 75\n  if (!configuredQualities?.length) return q\n  return configuredQualities.reduce(\n    (prev, cur) => (Math.abs(cur - q) \u003C Math.abs(prev - q) ? cur : prev),\n    configuredQualities[0]\n  )\n}\n\nfindClosestQuality(90, [75])           \u002F\u002F → 75 (only option)\nfindClosestQuality(82, [50, 75, 100])  \u002F\u002F → 75 (|82-75|=7 beats |82-100|=18)\nfindClosestQuality(90, [50, 75, 100])  \u002F\u002F → 100 (|90-100|=10 beats |90-75|=15)\n```\n\nTry the exact same function with your own numbers in the playground below — it runs this reduction live, not a simplified stand-in.\n\n\u003C!-- playground:start -->\n\n## 🎮 Try it yourself\n\n**[▶️ Open the interactive playground →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-image-priority-quality-changes\u002Fplayground)**\n\n_Runs right in your browser — poke at it and watch the concept react live._\n\n\u003C!-- playground:end -->\n\n### Stage 3 — configuring it on purpose\n\nIf 75 is too aggressive for a photography-heavy site, widen the allowlist explicitly:\n\n```js\n\u002F\u002F next.config.js\nmodule.exports = {\n  images: {\n    qualities: [40, 60, 75, 90, 100],\n  },\n}\n```\n\nNow `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`.\"\n\n### Stage 4 — the two different failure modes\n\nThis is the part that trips people up, because there are genuinely two code paths, not one:\n\n- **Through `\u003CImage>` (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.\n- **Through the raw optimization endpoint** (`GET \u002F_next\u002Fimage?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.\n\nSo \"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.\n\n## Walkthrough: priority to preload\n\n### Stage 1 — what `priority` always did\n\n`priority` on `\u003CImage>` 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 `\u003Clink rel=\"preload\">` for it, so the browser's preload scanner can start fetching it before React even renders the `\u003Cimg>` tag. That combination is exactly what you want for a Largest Contentful Paint (LCP) candidate — your hero image, the above-the-fold product photo.\n\n### Stage 2 — the rename, and why it's a rename and not a behavior change\n\n```tsx\n\u002F\u002F Next.js 15 — still compiles on 16, still works, but the type is now @deprecated\n\u003CImage src={hero} alt=\"…\" priority fill \u002F>\n\n\u002F\u002F Next.js 16 — the documented way forward\n\u003CImage src={hero} alt=\"…\" preload fill \u002F>\n```\n\n**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 `\u003Clink>` 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 `\u003CImage>` 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`\u002F`next start` production run.\n\n### Stage 3 — reaching past `preload` when you need more control\n\nThe 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:\n\n```tsx\n\u002F\u002F Multiple images could plausibly be the LCP element depending on viewport size —\n\u002F\u002F don't blanket them all with preload (that fights the browser's own prioritization)\n\u003CImage src={hero} alt=\"…\" loading=\"eager\" fetchPriority=\"high\" fill \u002F>\n```\n\n`preload` is a blunt, singular signal — \"fetch this one early, with a `\u003Clink>` tag.\" `loading`\u002F`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.\n\n## Edge cases and gotchas\n\n- **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.\n- **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.\n- **A static import's `quality` is checked too.** `quality` behaves identically whether `src` is a remote URL, a path under `public\u002F`, or a statically imported image — the allowlist check doesn't care how the `src` arrived.\n- **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.\n- **`priority`\u002F`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`\u002FCI pipeline will catch it. If you only ever test with a production build, an invalid combination can ship unnoticed.\n\n## Best practices\n\n- **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.\n- **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\"`\u002F`fetchPriority=\"high\"` on the candidates instead and let the browser's scanner decide.\n- **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.\n- **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.\n- **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`.\n\n## FAQ\n\n### Does `quality={90}` throw an error in Next.js 16?\n\nNo. 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.\n\n### Why does hitting `\u002F_next\u002Fimage?...&q=90` directly return a 400 but the component doesn't error?\n\nBecause they're different code paths. The `\u003CImage>` 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.\n\n### Is `priority` removed in Next.js 16?\n\nNo, 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 `\u003CImage>` reference records it as deprecated starting in `v16.0.0`.\n\n### Do I need to migrate `priority` to `preload` immediately?\n\nNot 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.\n\n### Does `images.qualities` affect images served through a custom loader?\n\nOnly 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 `\u002F_next\u002Fimage` endpoint in the typical case, so the strict-400 behavior doesn't apply there either.\n\n## Cheat sheet\n\n```js\n\u002F\u002F next.config.js — the Next.js 16 defaults, written out explicitly\nmodule.exports = {\n  images: {\n    qualities: [75],        \u002F\u002F REQUIRED allowlist since v16.0.0; unlisted quality → closest match\n  },\n}\n```\n\n| Task | Code | What actually happens |\n| --- | --- | --- |\n| Request a quality on the allowlist | `\u003CImage quality={75} \u002F>` | Requested exactly as given |\n| Request a quality NOT on the allowlist | `\u003CImage quality={90} \u002F>` (allowlist `[75]`) | Silently coerced to `75` (closest match); dev-only console warning |\n| Widen what's allowed | `images: { qualities: [50, 75, 100] } ` | `quality={90}` now resolves to `100` (closest), not `75` |\n| Hit the optimizer directly with a bad quality | `GET \u002F_next\u002Fimage?url=...&w=640&q=90` (not on allowlist) | **400 Bad Request** — no coercion on this path |\n| Mark the LCP \u002F hero image (current) | `\u003CImage preload fill \u002F>` | Eager load + `\u003Clink rel=\"preload\">` inserted |\n| Mark the LCP \u002F hero image (deprecated, still works) | `\u003CImage priority fill \u002F>` | Identical to `preload` — no runtime warning to switch |\n| Multiple possible LCP candidates | `\u003CImage loading=\"eager\" fetchPriority=\"high\" \u002F>` | Finer-grained signal than `preload`; lets the browser's scanner weigh several images |\n| Invalid combination (`next dev`-only error) | `\u003CImage priority preload \u002F>` or `\u003CImage priority loading=\"lazy\" \u002F>` | Throws: \"Only `preload` should be used\" \u002F \"Only one should be used\" — only while `NODE_ENV !== 'production'` |\n\n\n\u003C!-- quiz:start -->\n\n## 🧠 Test yourself\n\nThink it clicked? **[Take the 8-question quiz →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-image-priority-quality-changes\u002Fquiz)**\n\n_Instant feedback, a hint on every question, and an explanation for each answer — right or wrong._\n\n\u003C!-- quiz:end -->\n## Key takeaways\n\n- **`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 `\u003CImage>`, strictly (400) through the raw optimizer endpoint.\n- **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.\n- **`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.\n- **`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.\n\nYou 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.\n\nIf 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\u002Fimage changes with the fewest search results and the most \"wait, why did my image get softer\" GitHub issues.\n\nThis episode builds on two earlier ones in this series: [Next.js Cache Components Explained](https:\u002F\u002Fdev.to\u002Fparsajiravand\u002Fnextjs-cache-components-explained-with-cheat-sheet-55ob) covers the broader caching model `next\u002Fimage` sits alongside, and [Next.js Route Handlers: GET Stopped Caching in 15](https:\u002F\u002Fdev.to\u002Fparsajiravand\u002Fnextjs-route-handlers-get-stopped-caching-in-15-how-to-cache-in-16-56nb) is the same \"quiet default change with no error message\" pattern applied to Route Handlers instead of `\u003CImage>`.\n\n\u003C!-- related:start -->\n\n## 📚 Read next\n\n- [Next.js Cache Components Explained (with Cheat Sheet)](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-cache-components-explained)\n- [Next.js Parallel & Intercepting Routes: Modals Done Right](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-parallel-intercepting-routes-modals)\n- [Next.js Server Actions: Mutations & Security (Cheat Sheet)](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-server-actions-mutations-security)\n\n\u003C!-- related:end -->\n\n---\n\n🚀 **Want more like this?** Every guide, playground, and quiz lives on **[bestpractic.org](https:\u002F\u002Fbestpractic.org\u002F)** — open it and **[sign up free](https:\u002F\u002Fbestpractic.org\u002F)** so the next one finds you.\n\n*Thanks for reading! Let's stay connected:*\n\n- ⭐ **GitHub** — follow me and star the projects: [github.com\u002Fparsajiravand](https:\u002F\u002Fgithub.com\u002Fparsajiravand)\n- 💬 **Discord** — join the frontend best-practices community: [discord.gg\u002Fd9KRhuAwQ](https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ)\n- 📸 **Instagram** — frontend best practices, daily: [@bestpractice___](https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F)",{"title":110,"canonical":502,"description":111},"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-image-priority-quality-changes","01a120b9-d612-71fe-94b6-80161c750569",{"name":505,"part":506,"total":506,"items":507},"Next.js Deep Dive",7,[508,513,517,521,525,529,533],{"slug":509,"title":510,"publishedAt":511,"readingMinutes":512},"nextjs-weekly-cache-components-explained","Next.js Cache Components Explained (with Cheat Sheet)","2026-09-01T11:24:15.087Z",14,{"slug":514,"title":515,"publishedAt":516,"readingMinutes":113},"nextjs-weekly-server-actions-mutations-security","Next.js Server Actions: Mutations & Security (Cheat Sheet)","2026-09-08T10:57:00.975Z",{"slug":518,"title":519,"publishedAt":520,"readingMinutes":512},"nextjs-weekly-parallel-intercepting-routes-modals","Next.js Parallel & Intercepting Routes: Modals Done Right","2026-09-15T17:50:47.218Z",{"slug":522,"title":523,"publishedAt":524,"readingMinutes":100},"nextjs-weekly-middleware-to-proxy-network-boundary","Next.js proxy.ts Explained (with Cheat Sheet)","2026-09-22T19:36:08.416Z",{"slug":526,"title":527,"publishedAt":528,"readingMinutes":113},"nextjs-weekly-route-handlers-caching-streaming","Next.js Route Handlers: GET Stopped Caching in 15 — How to Cache in 16","2026-09-29T09:16:08.176Z",{"slug":530,"title":531,"publishedAt":532,"readingMinutes":512},"nextjs-weekly-streaming-metadata-head","Next.js Streaming Metadata: Why Your `\u003Chead>` Looks Incomplete","2026-10-06T06:10:31.468Z",{"slug":46,"title":110,"publishedAt":114,"readingMinutes":113},{"id":535,"locked":18},"01a120b9-d644-73e3-94f9-00f6fd0fa01c",[537],{"id":45,"slug":46,"title":48,"_count":538},{"questions":51},[540],{"locale":13,"slug":46},{"id":45,"slug":46,"title":48,"_count":542,"questionCount":51},{"questions":51}]