Preloading the LCP element
Cut Largest Contentful Paint with the Next.js preload prop, and keep the fast image defaults with a lint rule that bans the raw img tag.
The marketing page is the first thing a prospect sees, and Speed Insights reports its LCP at 4.1 seconds, well past the 2.5-second line that counts as good. The hero image is the largest thing in the viewport, so it is the LCP element, and it’s painting late.
Two changes fix this.
First, you mark the hero with Next.js 16’s preload prop (the prop earlier versions called priority), which front-loads the fetch of that one image and shaves a couple hundred milliseconds straight off the metric.
Second, a lint rule bans the raw <img> tag, so the fast image defaults you set up can’t quietly slip back out as the codebase grows.
Why the hero is discovered late
Section titled “Why the hero is discovered late”LCP is the time from navigation until the largest element paints, and that span breaks into a chain: the browser discovers the element in the markup, fetches its bytes, then paints it.
A hero <Image> in the body of your page loses time at the discover step.
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>, so the moment the browser sees those tags it starts downloading them.
The hero image sits lower down, in the <body>, and the browser can’t fetch an image it hasn’t read yet.
It doesn’t read that far until it has worked through the markup above and begun laying out the page; only then does it discover the hero, by which point the CSS and JS have a long head start.
On a real phone over a mobile connection, that gap adds 200 to 600 milliseconds straight onto LCP.
A second factor works against you: by default, next/image lazy-loads, holding off the fetch until the image is about to scroll into view.
That’s right for the avatar three screens down, where there’s no point spending bandwidth before the user scrolls there, but wrong for the hero, which is on screen from the first frame.
So the hero is penalized twice: discovered late, then told to wait.
You can’t fetch what you haven’t discovered, and preload moves the hero’s discovery up to <head>-parse time, so its bytes download alongside the CSS and JS instead of queuing behind them.
Scrub the timeline through three steps: the default late start, the same page with preload, and the trap the next section warns about.
What preload actually emits
Section titled “What preload actually emits”preload is one prop, but it does three concrete things to the rendered page.
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 one prop; the component emits all three. Here’s the authoring side, a fragment from inside your hero component:
<Image src={heroImage} alt="Invoices dashboard preview" preload sizes="100vw" />The sizes="100vw" is here because the hero spans the full viewport width; as you saw in the next/image lesson, sizes tells the browser which resolution to fetch.
It’s nothing new, it just belongs on a full-bleed image.
And here’s what the browser 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 also spot loading="eager" (the lazy-loading opt-out) and decoding="async".
The component picks those values for you; recognize them so the Elements panel reads cleanly when you check your work.
One preload per page: picking the LCP element
Section titled “One preload per page: picking the LCP element”Preload exactly one image per page: the one that is your LCP element. Every other image stays on the default lazy behavior, above the fold or not.
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, so neither lands as early as a single hero would have. Preloading everything is the same as preloading nothing, so the discipline is strict: pick the one element that defines LCP and leave the rest alone.
That means you have to know which element it is. You have two ways to find it:
- Chrome DevTools, Performance panel. Record a page load and the timeline drops an LCP marker pointing straight at the element the browser measured.
- PageSpeed Insights. Run your URL through it and the report names the LCP element from real field data.
The workflow is guess, then measure. At build time the LCP element is almost always the first big thing above the fold, a hero image or lead product photo, so mark that as your candidate. After the build, open the Performance panel and confirm the browser agrees.
Watch for one trap: above the fold is not the same as the LCP element. A header logo is above the fold and early in the DOM, but it’s tiny, so it’s never the largest contentful paint, and preloading it spends your budget on the wrong element.
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"?”You have three hints that sound alike, preload, fetchPriority, and loading="eager", and the skill is knowing which one a situation calls for and why reaching for more than one is a mistake.
Start with the common case, which covers most pages.
One stable LCP image, the same hero on every screen size, wants preload and nothing else.
It already implies fetchpriority="high" and eager loading, so it’s the complete answer.
Add it and stop.
The exception is when the LCP element changes with the viewport: a tall portrait hero on mobile, a wide landscape one on desktop, served as a deliberately different crop.
That’s art direction , and it turns preload into a liability.
A preload link is committed in the <head> before the browser knows which layout it will render, so it would front-load an image one of the two layouts never shows, wasting a download on the very connection you are 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.
The trap: never combine preload with loading or fetchPriority.
preload is a superset of both, so the browser ignores the duplicates and all you have added is confusion for the next reader.
Pick preload, or 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 that way: make the safe default impossible to bypass, so a single careless edit can’t quietly regress it.
Consider what a plain HTML <img> ships with: nothing.
It reserves no box for its dimensions, so the page jumps when it loads, the CLS from 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 optimizer.
And lazy-loading is something you have to opt into every time.
A raw <img> dropped in “just this once” silently re-introduces the exact failures next/image exists to prevent.
So the course makes it impossible to write: a raw <img> fails the lint check and doesn’t ship.
The rule is @next/next/no-img-element.
In Next.js’s base recommended config it is only a warning, which everyone scrolls past.
Adopting the eslint-config-next/core-web-vitals config upgrades every Core-Web-Vitals rule, no-img-element included, from warning to error, so a raw <img> blocks CI instead of nagging in the editor.
A fresh Next.js app ships with this config by default.
A word on the tooling, since 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.
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.
The short version: Biome is the primary linter, but the thing that bans <img> is the core-web-vitals ESLint config.
Here’s what the rule enforces:
<img src="/hero.png" alt="Invoices dashboard preview" />Fails the lint check. One tag undoes everything next/image gives you: no reserved box, no srcset, no optimizer, no automatic lazy-loading.
<Image src={heroImage} alt="Invoices dashboard preview" preload sizes="100vw" />The enforced default. Sized box, responsive srcset, the optimizer, and preload front-loading the LCP fetch, all from the component the lint rule forces you to use.
The wiring is three lines: spread the core-web-vitals config, which flips no-img-element to an error.
import coreWebVitals from 'eslint-config-next/core-web-vitals';
export default [...coreWebVitals];There’s exactly one exception.
Inside MDX or markdown content, say an article body 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.
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”After you add preload, confirm it in two places that update on very different schedules.
The Network panel updates immediately, in the same session.
Reload the page with the Network tab open and find the hero image.
Its Initiator should read “Parser” (started from the preload link in the parsed <head>, not discovered later in the body) and its Priority should read High.
It should begin downloading within roughly 200 ms of navigation start, alongside the bundle.
That confirms the fetch moved up.
Speed Insights updates slowly. Your lab numbers improve right away, but the field LCP on the production dashboard lags, because field data is a 28-day rolling window. The production pill can stay red for a week or two after a real fix ships, so don’t undo a good change because the field number hasn’t caught up.
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; the bottleneck is elsewhere in the chain the last lesson taught you to read.
A slow TTFB, for instance, means the server is the problem (covered later in this chapter), not the image.
To finish, 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=___ ___/>One history note: older Next.js code and most online tutorials write this as priority rather than preload.
priority is the deprecated name that Next.js 16 renamed to preload.
It still works, but write preload, and treat priority as the alias you’ll recognize in old code.
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.