Priority on the LCP element
Front-load your hero image with the Next.js preload prop to cut Largest Contentful Paint, and lock the win in with a lint rule that bans the raw img tag.
The marketing page is the first thing a prospect sees, and Speed Insights says its LCP is 4.1 seconds, well past the 2.5-second line that counts as good. In the last lesson you learned to read that number and trace it to a cause, and the cause here is clear: the hero image is the largest thing in the viewport, so the hero image is the LCP element, and it’s painting late. That lesson named the fix in one line and promised the details here, so this is where you get them.
The fix has a naming wrinkle worth clearing up first.
The last lesson called it “marking the image priority,” and priority is still the right way to think about what you’re doing: you’re telling the browser that this one fetch matters more than the rest.
But the prop that does it in Next.js 16 is called preload.
It’s the same idea under the current name, and there’s a short history note on the old name later in the lesson.
This lesson makes two small claims.
First, preload front-loads the fetch of the one image that is your LCP element, shaving a couple hundred milliseconds straight off the metric.
Second, a lint rule bans the raw <img> tag, so the safe, fast image defaults you set up can’t quietly slip back out as the codebase grows.
You already know the <Image> component itself, every prop, the optimizer, and remote images, from when you first met next/image earlier in the course.
This lesson doesn’t re-teach any of that.
It takes one thin slice and answers a sharper question: how do you make your single most important image fast, and how do you stop a teammate from undoing it next sprint?
Why the hero loads late by default
Section titled “Why the hero loads late by default”Start with the puzzle the last lesson set up but didn’t open.
LCP is the time from navigation until the largest element paints, and that span breaks into a chain: the HTML arrives, the browser discovers the element in the markup, it fetches the element’s bytes, then it paints.
A normal <Image> in the body of your page loses time at the discover step, and the reason why is worth understanding, because everything else in this lesson is mechanics layered on top of this one idea.
Here’s the sequence.
The browser receives your HTML as a stream of text and builds the page top to bottom.
Your CSS and JavaScript are referenced in the <head>, right at the top, so the moment the browser sees those tags it starts downloading them, and they get a head start measured in milliseconds from the very first byte.
Your hero image sits lower down, in the <body>.
The browser can’t fetch an image it hasn’t read yet, and it doesn’t read that far until it has worked through the markup above it and started laying the page out.
Only then does it discover the hero and begin downloading it, by which point the CSS and JS have had a long head start.
On a real phone over a real mobile connection, that discovery gap adds 200 to 600 milliseconds straight onto LCP.
A second factor works against you.
By default, next/image lazy-loads: it deliberately holds off fetching an image until it’s about to scroll into view.
That’s the right call for the avatar three screens down the page, since there’s no point spending bandwidth on it before the user scrolls there.
But it’s the wrong call for the hero, which is on screen from the first frame.
So the hero is penalized twice: discovered late, then told to wait.
That gives you the whole mental model: you can’t fetch what you haven’t discovered, and preload moves the hero’s discovery up to <head>-parse time, so its bytes start downloading alongside the CSS and JS instead of queuing behind them.
The timeline below makes this concrete.
Scrub through the three steps: the default late start, the same page with preload added, and the trap that the next section warns you about.
Hold onto that third step, because it sets up the next rule.
What preload actually emits
Section titled “What preload actually emits”preload is one prop, but it does three concrete things to the rendered page.
Knowing all three lets you open DevTools and verify the fix instead of trusting that it worked.
When you add preload to an <Image>, Next.js:
- Injects a
<link rel="preload" as="image" ...>into the document<head>. That’s the line that tells the browser to start the download early. - Sets
fetchpriority="high"on the underlying<img>, so the fetch jumps the queue. - Opts the image out of lazy-loading, so it starts immediately rather than waiting to scroll into view.
You write none of those three by hand. You write one prop and the component emits all of it. Here’s the authoring side, a fragment from inside your hero component rather than a full file:
<Image src={heroImage} alt="Invoices dashboard preview" preload sizes="100vw" />The sizes="100vw" is there because the hero spans the full width of the viewport, and you already know from the next/image lesson that sizes tells the browser which resolution to fetch.
It’s nothing new here; it simply belongs on a full-bleed image, so it rides along.
And here’s what the browser actually receives once that renders, the part you’ll recognize in the DevTools Elements panel:
<link rel="preload" as="image" href="/_next/image?url=...&w=1920" /><!-- ...later, in the body... --><img src="/_next/image?url=...&w=1920" alt="Invoices dashboard preview" sizes="100vw" fetchpriority="high" loading="eager" decoding="async"/>You’ll notice loading="eager" (the opt-out of lazy-loading) and decoding="async" in there too.
You don’t set those yourself; the component picks sensible values. It’s worth recognizing them, though, so the Elements panel reads cleanly when you go to check your work.
One preload per page: picking the LCP element
Section titled “One preload per page: picking the LCP element”Now the rule that third diagram step was setting up: preload exactly one image per page, the one that is your LCP element. Every other image on the page stays on the default lazy behavior, whether it’s above the fold or not.
The reason is the one the diagram showed. High priority is a budget the browser spends, not a switch that makes everything fast. Mark two images high-priority and the browser splits its attention between them, so now neither lands as early as the single hero would have. Preloading everything is the same as preloading nothing, which is why the discipline is strict: pick the one element that defines LCP, and leave the rest alone.
So you need to know which element that is.
This is the measurement question the next/image lesson explicitly deferred, and now you have the tools for it, because you met the DevTools Performance and Network panels back when you first learned the browser platform.
There are two ways to find your LCP element:
- Chrome DevTools → Performance panel. Record a page load, and the timeline drops an LCP marker that points directly at the element the browser measured. You don’t have to guess; it tells you.
- PageSpeed Insights. Run your URL through it and the report highlights the LCP element straight from real field data.
The workflow mirrors the field-versus-lab spine from the last lesson: guess, then measure. At build time the LCP element is almost always the first big thing above the fold, a hero image or a lead product photo, so you mark that as your candidate. Then after the build you open the Performance panel and confirm the browser agrees.
Watch out for one trap: above the fold is not the same as the LCP element. A logo in the header is above the fold and early in the DOM, but it’s tiny, so it’s never the largest contentful paint. Preloading it spends your budget on the wrong element.
Try this before moving on.
A landing page renders, top to bottom: a small company logo in the header, then a full-bleed hero photograph that fills the viewport, then a row of two small product thumbnails, then a circular user avatar in a testimonial. Every one of these is above the fold. Which single element should get preload?
preload, fetchPriority, or loading="eager"?
Section titled “preload, fetchPriority, or loading="eager"?”There’s a related decision worth getting right, because the next/image lesson only gestured at it and it’s where people reach for the wrong tool.
You have three hints that all sound similar, preload, fetchPriority, and loading="eager", and the skill is knowing which one the situation calls for, and why reaching for more than one is a mistake.
Start with the common case, which covers the large majority of pages.
One stable LCP image, the same hero on every screen size, wants preload and nothing else.
Since preload already implies fetchpriority="high" and eager loading, it’s the complete answer.
Add it and stop.
Now the one wrinkle.
Sometimes the LCP element changes depending on the viewport: a tall portrait hero on mobile, a wide landscape one on desktop, served as a deliberately different crop.
That’s called art direction , and it’s the case where preload turns into a liability.
A preload link is committed in the <head> before the browser even knows which layout it’s rendering, so it would front-load an image that one of the two layouts never displays, wasting a download on exactly the connection you’re trying to protect.
Here you reach for fetchPriority="high" (optionally with loading="eager") on the image that is shown, so the urgency hint travels with the element the layout actually paints instead of being hard-wired into the head.
And the trap that catches people: never combine preload with loading or fetchPriority.
preload is a superset of both, so setting them alongside it is redundant: the browser ignores the duplicates, and all you’ve added is confusion for the next person reading the code.
Pick preload, or pick the eager and fetchPriority pair for the art-directed case, never both.
Here’s the whole decision as a lookup:
| Situation | Reach for | Why |
| --- | --- | --- |
| One stable LCP image, same on every viewport | preload | Complete answer: it implies high fetch priority and eager loading. |
| LCP element differs by viewport (art direction) | fetchPriority="high" (± loading="eager") | Hint follows the element actually painted; a <head> preload would fetch an unused image. |
| Anything else, like below-the-fold or secondary images | nothing | Stay on the default lazy behavior; spending priority here steals it from the hero. |
The ban on raw <img>
Section titled “The ban on raw <img>”Everything so far makes the hero fast. This last piece keeps it fast, and it’s the structural side of the chapter’s spine: make the safe default impossible to bypass, so it can’t quietly regress.
Consider what a plain HTML <img> tag ships with: nothing.
It reserves no box for its dimensions, so the page jumps when it loads, which is the CLS you learned to watch for in the last lesson.
It carries no responsive srcset, so a phone downloads the full desktop-resolution file: oversized bytes, slower LCP.
It runs through no automatic optimization pipeline.
And lazy-loading is something you have to remember to opt into, every single time.
A single raw <img> dropped in “just this once” silently re-introduces the exact failures next/image exists to prevent.
The defense is to make it impossible to write.
The course turns “please remember to use <Image>” into “a raw <img> fails the lint check and doesn’t ship,” and it does that with a specific, named rule.
The rule is @next/next/no-img-element.
Out of the box, in Next.js’s base recommended config, this rule is only a warning, and a warning is something everyone scrolls past.
It becomes an error the idiomatic way, by adopting the eslint-config-next/core-web-vitals config, which upgrades every Core-Web-Vitals-affecting rule, no-img-element included, from warning to error.
This is the config a fresh Next.js app ships with by default.
So the accurate description isn’t “we hand-wrote a rule override”; it’s “the project uses the core-web-vitals config, so a raw <img> fails the lint run and blocks CI rather than just nagging in the editor.”
A warning is a suggestion; an error stops the build.
The tooling underneath is worth being precise about, because it’s easy to get wrong.
The course’s primary linter is Biome , not ESLint.
But Biome has no equivalent for this rule, because it’s Next-specific.
So this one rides in through ESLint, via the @next/eslint-plugin-next plugin (bundled in eslint-config-next), configured in the flat config file, eslint.config.mjs.
ESLint runs alongside Biome precisely for Next-specific rules like this one that Biome can’t cover.
One more change to keep in mind: Next.js 16 removed the old next lint command and the eslint key in the Next config, so linting now runs straight through the ESLint CLI against that flat config.
You don’t need to author an ESLint config from scratch. Just hold the accurate model: Biome is the primary linter, but the thing that bans <img> is the core-web-vitals ESLint config.
Here’s the before and after the rule enforces:
<img src="/hero.png" alt="Invoices dashboard preview" />Fails the lint check. No reserved box (which re-introduces CLS), no srcset (so a phone downloads the full desktop file), no optimizer, and lazy-loading you’d have to remember. The one tag undoes everything next/image gives you.
<Image src={heroImage} alt="Invoices dashboard preview" preload sizes="100vw" />The enforced default. Sized box (no layout shift), responsive srcset, the optimizer, and preload front-loading the LCP fetch, all from the component the lint rule forces you to use.
If you ever want to see the wiring, it’s three lines. You spread the core-web-vitals config, and that’s what flips no-img-element to an error:
import coreWebVitals from 'eslint-config-next/core-web-vitals';
export default [...coreWebVitals];There’s exactly one legitimate exception, and it’s worth naming so you don’t think the ban is absolute.
Inside MDX or markdown content, say an article body coming from a CMS, authors write a plain <img>, and that’s fine: the MDX renderer maps those to next/image at compile time, so the optimizer still applies, and the lint rule shouldn’t pick fights with authored prose.
That’s the carve-out.
In your own feature code there are no exceptions: the hero, the avatars, and the product shots all go through <Image>.
Verifying the fix
Section titled “Verifying the fix”A fix you can’t verify is a guess, so close the loop. There are two places to look, and they move on very different clocks.
The fast one is the Network panel: same session, immediate feedback.
After you add preload, reload the page with the Network tab open and find the hero image.
Two things confirm the hint landed. Its Initiator reads “Parser” (it was started from the preload link in the parsed <head>, not discovered later in the body), and its Priority reads High.
You should see it begin downloading within roughly 200 ms of navigation start, right alongside the bundle.
That’s your proof the fetch moved up.
The slow one is Speed Insights, and this is where people panic. Your lab numbers improve right away, but the field LCP on the production dashboard lags, because, as the last lesson covered, field data is a 28-day rolling window. So the production pill can stay red for a week or two after a real fix has shipped. That’s not the fix failing; it’s the window doing its job as a stability feature. Don’t undo a good change because the field number hasn’t caught up yet.
One last guardrail, because it’s the reflex that wastes the most time.
preload fixes discovery latency, and only that.
If LCP is still bad after you’ve preloaded the right element, a second preload won’t help, because the bottleneck is somewhere else in the chain the last lesson taught you to read.
A slow TTFB means the server is the problem (covered later in this chapter), not the image.
Reach for the diagnostic order first instead of throwing more preloads at it.
To close, wire up a full-bleed hero so it becomes the preloaded LCP element. Fill the two blanks.
This is the full-bleed hero — the LCP element on the page. Fill in the responsive width hint and the prop that front-loads its fetch. Pick the right option from each dropdown, then press Check.
<Image src={heroImage} alt="Invoices dashboard preview" sizes=___ ___/>And the history note that was promised at the top: in older Next.js code and most tutorials online, you’ll see this written as priority rather than preload.
priority is the deprecated name for the same thing, which Next.js 16 renamed to preload.
It still works for now, but write preload. Treat priority as the alias you’ll recognize in old code, not the one you reach for.
External resources
Section titled “External resources”Reach for these when you want the source of truth on the props or the platform theory behind preloading.
The preload / fetchPriority section and the priority-deprecated note — the source of truth as the prop names settle.
The platform-agnostic theory behind preloading the LCP resource, independent of any framework.
The rule that bans raw <img> and the core-web-vitals config that turns it into an error.
Why fetchpriority tunes urgency but not discovery — the distinction behind picking it over preload for art-directed heroes.