LCP Preload Hint Picking the Wrong Image: Real Fix

LCP regressed because the browser preloads the wrong image with next/image priority? Fix fetchpriority, srcset, and preload candidate selection with code.
PerformanceCore Web VitalsNext.js
April 25, 20266 min read1049 words

The Problem

Pushed a redesign to staging on Wednesday, ran PageSpeed Insights, and LCP had jumped from 1.8s to 3.4s on mobile. The hero was a next/image with priority set, which is supposed to inject a <link rel="preload"> and short-circuit LCP. Looking at the network waterfall, the preload was firing, just for the wrong image. Chrome was preloading a 28KB SVG logo from the header instead of the 180KB hero JPEG underneath it.

If your LCP regressed and the offender is an image you tagged as priority, you are probably hitting the preload candidate selection rules that landed in Chrome 122 and tightened in 134. The fix is not "add more priority hints"; that makes it worse. Here is what is actually going on and how to point the preloader at the right element.

Why It Happens

When Chrome sees <link rel="preload" as="image" fetchpriority="high"> in <head>, it treats every matching candidate as a high-priority load. Two changes broke the simple model.

Change 1: imagesrcset is now matched against viewport before fetch. Before Chrome 122, a preload with imagesrcset would just download the largest entry. Now Chrome runs the same selection logic the <img> tag will run, picks the candidate that matches the current viewport width, and downloads only that one. If your next/image priority hero has a sizes="100vw" and the logo above it has no sizes (so it defaults to 100vw from next/image), the logo wins on a phone because its smallest srcset entry is the cheapest.

Change 2: Fetchpriority high is awarded by element position. When two preloads both have fetchpriority="high", Chrome breaks the tie by document order. The first one in source order wins the network slot. A header logo declared in your RootLayout runs before the page-level hero in source order, so the logo gets the high-priority slot and the hero falls to medium.

Change 3: Above-the-fold detection runs after layout. The browser does not know which image is the LCP candidate until layout finishes. The preload runs from the document head, before layout. So the preload guesses, and the heuristic is "first large image in source order with fetchpriority="high"", which is your logo.

The result: your real LCP image starts downloading after the logo has finished and after the layout has happened. On a slow 4G connection that is a 1.5–2 second penalty.

The Fix

Step 1: Stop using priority on anything that is not the LCP element. The most common mistake I see is priority on logos, hero icons, and "above the fold" thumbnails. priority on next/image injects a preload with fetchpriority="high". You only want one, and it must be the LCP.

// Wrong
<Image src="/logo.svg" priority alt="Logo" width={120} height={32} />
<Image src="/hero.jpg" priority alt="Hero" width={1600} height={900} />

// Right
<Image src="/logo.svg" alt="Logo" width={120} height={32} />
<Image src="/hero.jpg" priority fetchPriority="high" alt="Hero" width={1600} height={900} sizes="100vw" />

If you are unsure which element is the LCP, run this in DevTools Console and reload:

new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log('[LCP]', entry.element, entry.startTime)
  }
}).observe({ type: 'largest-contentful-paint', buffered: true })

The last entry before user input is your real LCP.

Step 2: Set sizes on every responsive image. A missing sizes attribute means next/image falls back to 100vw, and Chrome then downloads the largest srcset entry. Be specific:

<Image
  src="/hero.jpg"
  alt="Headline"
  width={1600}
  height={900}
  priority
  fetchPriority="high"
  sizes="(min-width: 1024px) 1280px, 100vw"
/>

That sizes value tells Chrome to download a 1280px-wide version on desktop and a viewport-width version below 1024px. The preloader matches against the actual width, not the largest, and you stop wasting bytes on the wrong candidate.

Step 3: Preload manually if you need precise control. When you have multiple candidate "heroes" (a video poster, a hero image, a background SVG), the safest move is to drop priority entirely and inject your own preload in the page-level layout:

// app/page.tsx
import Head from 'next/head'

export default function Home() {
  return (
    <>
      <link
        rel="preload"
        as="image"
        href="/hero-1280.jpg"
        imageSrcSet="/hero-640.jpg 640w, /hero-1280.jpg 1280w, /hero-1920.jpg 1920w"
        imageSizes="(min-width: 1024px) 1280px, 100vw"
        fetchPriority="high"
      />
      <Hero />
    </>
  )
}

Inject this in the page, not the root layout. Page-level preloads sit below layout-level resources in source order, but for an explicit preload tag with fetchpriority="high", that does not matter; the high-priority queue beats document order.

Step 4: Verify with a fresh Lighthouse and the Chrome Network priority column. Open DevTools → Network, right-click any column header, and tick "Priority". Reload the page. Your LCP image should be Highest priority. Anything else marked Highest is competing for the slot. Demote it (remove priority, drop the fetchPriority attribute, or remove the manual preload) until your hero is alone at the top.

You can also check this from the field side. Add a quick web-vitals listener for LCP regressions:

'use client'

import { useEffect } from 'react'
import { onLCP } from 'web-vitals'

export function LcpReporter() {
  useEffect(() => {
    onLCP((metric) => {
      if (metric.value > 2500) {
        const entry = metric.entries.at(-1)
        console.warn('[LCP regression]', metric.value, entry?.element)
      }
    })
  }, [])
  return null
}

If entry.element is not the element you expect, your preload is still wrong. The web.dev preload critical assets guide covers the full priority semantics.

The Lesson

priority on next/image is not a "this is important" flag. It is "preload this exact element as the LCP candidate." Use it on exactly one element per route, set sizes on every responsive image, and verify with the Network priority column before you trust your fix. If you have multiple hero candidates, drop priority everywhere and inject the preload manually.

If your Core Web Vitals are stuck after a redesign and you need someone to unblock LCP fast, I do this kind of work — see my services, or for the related issue I hit last week, see next/font CLS layout shift in Next.js 16.

Back to blogStart a project