The Problem
Bumped a client e-commerce site from Next.js 15.4 to 16.2 last weekend. The build went through clean. By Monday, PageSpeed Insights field data showed mobile LCP up from 2.1s to 3.4s on every product page. Nothing in the build had changed except the framework version. The hero product image was still using next/image with priority, the warning in the build log was the only hint:
[next/image] The "priority" prop is deprecated and will be removed in Next.js 17.
Use "preload" instead. See https://nextjs.org/docs/messages/image-priority-deprecated
If your LCP regressed after a Next.js 16 upgrade and you are still passing priority, the prop is being silently ignored on some routes. Here is what changed and how to land back under 2.5s.
Why It Happens
priority did three things in 15. It set loading="eager", set fetchpriority="high", and emitted a <link rel="preload" as="image"> tag in the document head. Three behaviors, one prop, no way to opt out of any one of them.
Next.js 16 splits that into two separate props with different defaults:
preload={true}emits the<link rel="preload">tag and setsfetchpriority="high"loading="eager"controls only the lazy-loading attribute
The deprecated priority prop still gets read for backwards compatibility, but the runtime behavior shifted in three ways.
Shift 1: priority no longer emits the preload tag on streamed routes. If your route uses loading.tsx or any Suspense boundary above the image, the preload tag would arrive after the LCP image element in the streamed HTML, which the browser ignores. Next.js 16 detects this and skips the preload entirely rather than emit a useless tag. The image still gets fetchpriority="high" but loses the connection warm-up, which on slow mobile is worth 300-600ms of LCP.
Shift 2: preload enforces a single hero per page. The new preload prop logs a build warning if more than one image on a route declares it. The old priority prop did not. Sites that had priority on every hero in a carousel were silently preloading 5-8 images, slowing all of them. Next.js 16 forces you to pick one.
Shift 3: Image domains in remotePatterns need a formats whitelist for preload. If your image is on a CDN and the URL does not match a remotePatterns entry that explicitly lists formats: ['image/webp', 'image/avif'], the preload tag falls back to the original format. On a cached AVIF image, that means the browser preloads the JPEG, then downloads the AVIF for actual render. Two requests, no benefit.
The Fix
Step 1: Rename priority to preload on exactly one image per route. This is the migration the warning is telling you to do, but only on the confirmed LCP element. Use Lighthouse to confirm which one:
import Image from 'next/image'
import hero from './hero.jpg'
export default function ProductPage() {
return (
<main>
<Image
src={hero}
alt="Premium leather jacket on a model"
width={1200}
height={800}
preload
sizes="(max-width: 768px) 100vw, 50vw"
/>
</main>
)
}
If you have a carousel, only the first slide gets preload. Slides 2+ should be regular next/image without loading="lazy" (so they are eager but not preloaded).
Step 2: Lock the format in next.config.ts for CDN images. If your hero is on Cloudflare, Cloudinary, or any third-party CDN, declare the formats explicitly:
import type { NextConfig } from 'next'
const config: NextConfig = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'cdn.example.com',
pathname: '/products/**',
},
],
formats: ['image/avif', 'image/webp'],
minimumCacheTTL: 31536000,
},
}
export default config
formats order matters. The browser receives an Accept header listing what it supports, and Next.js negotiates the first matching format from your list. AVIF first means modern browsers get the smallest payload, WebP second, then the original.
Step 3: Verify the preload tag actually fires. Open the route in Chrome DevTools, switch to the Network tab, throttle to "Slow 4G", and reload. Filter to "Img". The hero should show up before any of the other downloads, with Initiator listed as (Other) - <link rel=preload>. If the initiator is the <img> element, the preload tag did not fire and you have a Suspense boundary above the image hiding it from the streamed head.
When that happens, hoist the image. Either move it above the Suspense boundary, or split the route so the LCP image renders synchronously. The Next.js Image component docs cover the streaming caveat.
Step 4: Field-measure LCP with web-vitals. Lighthouse runs synthetic loads with a fixed network profile. Real users hit you on real networks. Drop this in a client component mounted in your root layout:
'use client'
import { useEffect } from 'react'
import { onLCP } from 'web-vitals'
export function LcpReporter() {
useEffect(() => {
onLCP((metric) => {
const url = new URL('/api/vitals', window.location.origin)
const body = JSON.stringify({
name: 'LCP',
value: metric.value,
id: metric.id,
url: window.location.pathname,
element: (metric.entries[0] as LargestContentfulPaint | undefined)
?.element?.tagName,
})
navigator.sendBeacon(url, body)
})
}, [])
return null
}
The entries[0].element.tagName field tells you whether the LCP element is your IMG, an H1, a DIV (background image), or a video poster. If field LCP is good but Lighthouse LCP is bad, that is a synthetic-only issue, usually a slow third-party script that does not run on real users.
Step 5: Audit which routes still use priority. Before shipping the migration, find every remaining call site:
rg -n 'priority' --type tsx --type ts \
--glob '!**/node_modules/**' \
--glob '!**/.next/**' \
app components
Replace priority with preload on the LCP element only. Delete it everywhere else. The loading="eager" default for above-the-fold images already does what most of the stragglers needed.
The Lesson
The 16 split between preload and loading="eager" is overdue. It forces you to declare the one image worth preloading per route instead of stamping priority on every hero in sight. Migrate, verify the preload tag fires in DevTools, and lock your CDN formats so the preload payload matches the rendered image. LCP gets back to where it was, often better.
If your Core Web Vitals tanked after a Next.js 16 upgrade and you want someone to unwind it, that is the kind of perf work I do — see my services, or read the full Core Web Vitals 2026 guide for the other regressions to watch out for.