next/image priority Not Preloading Hero: LCP Fix

next/image with priority not generating a preload link and tanking your hero LCP score? Fix the missing preload hint and PPR boundary issue with code.
PerformanceCore Web VitalsNext.js
May 1, 20266 min read1001 words

The Problem

Pulled up a client's marketing site in Lighthouse on Wednesday morning and the LCP was 3.4s on a 4G profile. The hero is a single 120KB WebP photograph at the top of the page, served from Vercel's image CDN. We had priority on the <Image> component. Should be a half-second LCP, not three.

View source. No <link rel="preload" as="image"> in the head. Open DevTools, network panel, filter for the hero image, and the request fires after the JS bundle has parsed. So priority is being declared but not honoured, and the browser is discovering the image during render instead of being told about it in the response headers.

If you have set priority on your hero <Image> and the preload link is not in your HTML, you have hit one of the four boundary issues that disable next/image preloading silently. Here is the diagnosis.

Why It Happens

priority does two things. It sets fetchpriority="high" and loading="eager" on the <img> tag, and it asks Next.js to inject a <link rel="preload" as="image"> into the document head. The first part always works because it is an attribute on the rendered tag. The second part has four ways to fail in Next.js 16.

Failure 1: The image lives inside a Client Component boundary. Next.js can only inject the preload link if it sees the <Image priority> during the server render of the route. If your hero sits inside a Client Component (anything with 'use client' or any descendant of one), the server sees an empty boundary and skips the preload. The <img> still renders with fetchpriority="high", which tricks people into thinking it works.

Failure 2: The image is inside a deferred PPR boundary. With Partial Prerendering, anything inside the dynamic shell cannot have a preload link in the prerendered HTML, because the prerender does not know what will render there. Putting priority on a streamed image is wasted.

Failure 3: The image source is dynamic at request time. If src comes from a CMS fetch inside a 'use cache' Server Component that has not been warmed yet, the first request renders without knowing the URL. Cold cache, no preload.

In our client's case it was Failure 1. The hero sat inside a <HeroCarousel> Client Component, so the image tree was client-rendered.

The Fix

Step 1: Verify with view-source, not DevTools. DevTools shows the live DOM after hydration, which can include client-injected links. View-source shows what arrived from the server:

curl -s https://yoursite.com | grep -i 'rel="preload".*as="image"'

If that returns nothing, the preload is not in the response. Everything else is downstream of fixing that.

Step 2: Move the priority Image to a Server Component. Even if your hero needs a carousel, the first frame can be server-rendered. Refactor:

// app/page.tsx (Server Component)
import Image from 'next/image'
import HeroCarousel from './hero-carousel'
import heroImg from '@/public/hero.webp'

export default function Home() {
  return (
    <section className="hero">
      <Image
        src={heroImg}
        alt="Pakistan Highway 35 at sunrise"
        priority
        fetchPriority="high"
        sizes="100vw"
        className="hero-img"
      />
      <HeroCarousel />
    </section>
  )
}

The <HeroCarousel> Client Component handles the rest of the slides, but the first frame is a static, server-rendered <Image> with priority. Next.js sees it during the server render, the preload link goes into the head, the image arrives early, LCP drops.

Step 3: Use a static import for the src. When you import the image as a module (import heroImg from '@/public/hero.webp'), Next.js knows the dimensions and the URL at build time. That lets it preload the exact responsive variant for the user's device. If you pass a string src, Next.js has to compute the variant at request time, which delays the preload.

Step 4: Set sizes precisely. The preload link includes the imagesizes attribute, which the browser uses to pick the right variant from imagesrcset. Vague sizes like 100vw work but are wasteful, since you preload the desktop image on mobile. Be specific:

<Image
  src={heroImg}
  alt="..."
  priority
  sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
/>

This shaves real kilobytes off the LCP image on mobile, where LCP matters most.

Step 5: Manual preload for unavoidable Client Components. If you cannot move the hero out of a Client Component (third-party widget, locked-down design system), inject the preload yourself in app/layout.tsx:

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <head>
        <link
          rel="preload"
          as="image"
          href="/hero.webp"
          fetchPriority="high"
          imageSizes="(max-width: 640px) 100vw, 50vw"
          imageSrcSet="/hero-640.webp 640w, /hero-1280.webp 1280w"
        />
      </head>
      <body>{children}</body>
    </html>
  )
}

Belt and suspenders. The imageSrcSet and imageSizes give the browser everything it needs to pick the right variant without rendering the image first. Validate the headers using WebPageTest on a 4G profile, not just local Lighthouse.

Step 6: Confirm the fix in field data. Synthetic Lighthouse can lie. Add web-vitals and post onLCP to your analytics endpoint, then watch real CrUX for a week. On the client site, p75 LCP dropped from 3.4s to 1.1s within four days of the fix landing.

The Lesson

priority on <Image> is a hint, not a guarantee. The preload link only fires when the image is server-rendered, not nested inside a Client Component, and not inside a deferred PPR boundary. Verify with view-source, move the hero to the Server Component, and add a manual <link rel="preload"> only if you cannot refactor.

If your LCP is bad and you have no idea why even after setting priority, this is the kind of work I do on retainer — see my services, or read next/font CLS layout shift fix for the matching font-side regression.

Back to blogStart a project