Next.js8 min read
Next.js Image and Font Optimization for Core Web Vitals
Next.js image and font optimization for Core Web Vitals: next/image sizes and preload, AVIF, next/font fallbacks, and fixes for slow LCP and layout shift.
To optimize images and fonts in Next.js for Core Web Vitals, use next/image with correct width/height or fill plus sizes, load only the LCP image eagerly (loading="eager" or fetchPriority="high", or preload when it is discovered late), and load fonts with next/font so they are self-hosted, preloaded and paired with a size-adjusted fallback. Those few changes target the two metrics images and fonts affect most: Largest Contentful Paint (LCP) and Cumulative Layout Shift (CLS).
This guide covers what changed in Next.js 16 (the priority prop is deprecated, image defaults moved) and the mistakes that keep LCP slow even when you "use next/image".
Key takeaways
- Google's "good" thresholds are LCP ≤ 2.5s, INP ≤ 200ms and CLS ≤ 0.1 at the 75th percentile (web.dev).
next/imageprevents CLS by reserving space, but only if you give it dimensions orfillinside a sized parent.- Since Next.js 16,
priorityis deprecated. Useloading="eager"orfetchPriority="high"on the LCP image, andpreloadonly when you need a<link>in the head. - Always set
sizeson responsive andfillimages, otherwise the browser may download a much larger file than it displays. next/fontself-hosts fonts and generates a metric-matched fallback to cut layout shift from font swaps.- Next.js 16 changed image defaults:
qualitiesis[75],minimumCacheTTLis 4 hours, and16was removed fromimageSizes.
How images and fonts affect Core Web Vitals
On most marketing pages, blog posts and product pages, the LCP element is either a hero image or a large heading. That means:
- Image LCP depends on how early the browser discovers the image, how big the file is, and whether anything delays it (lazy loading, client-side rendering).
- Text LCP depends on how quickly the web font is ready, or how good the fallback is while it loads.
- CLS comes from images without reserved space and from fonts that swap in at a different size than the fallback.
INP is mostly about JavaScript, so it is out of scope here. I cover it in Core Web Vitals for React: INP, LCP and CLS.
Next.js image optimization with next/image
next/image serves resized, modern-format images through the built-in optimizer, lazy loads by default, and generates a srcset.
Local images
Static imports give Next.js the dimensions automatically and a free blur placeholder:
// app/page.tsx
import Image from 'next/image'
import hero from './hero.jpg'
export default function Page() {
return (
<Image
src={hero}
alt="Painted prayer flags above a Kathmandu courtyard"
placeholder="blur"
loading="eager"
fetchPriority="high"
sizes="100vw"
style={{ width: '100%', height: 'auto' }}
/>
)
}Remote images
Remote images need explicit width and height (or fill) and an allowlist in next.config.ts. Since 15.3, remotePatterns accepts URL objects:
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
images: {
remotePatterns: [new URL('https://cdn.example.com/artworks/**')],
formats: ['image/avif', 'image/webp'],
},
}
export default nextConfigWith both formats listed, browsers that accept AVIF get AVIF and the rest fall back to WebP. Order matters: the first match in the array wins. AVIF files are usually smaller but take longer to encode on first request, so the first visitor to a new size pays that cost.
Keep patterns narrow. Omitting pathname or search implies a wildcard, which lets anyone use your optimizer for URLs you never intended. images.domains is deprecated in Next.js 16.
The sizes prop is where most bandwidth goes
Without sizes, a responsive image assumes it may be as wide as the viewport and picks from the large end of srcset. For a product card that is one third of the screen on desktop:
<div className="relative aspect-[4/5]">
<Image
src={artwork.imageUrl}
alt={artwork.title}
fill
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
className="object-cover"
/>
</div>A fill image needs a parent with position: relative (or absolute/fixed) and a real size. The aspect-[4/5] class gives it height before the image arrives, which is what prevents CLS.
On Annapurna Art, a Next.js art marketplace I built, the artwork images are the product, so grid pages are exactly this pattern: fill, an aspect-ratio wrapper and an honest sizes string per breakpoint.
Fixing LCP: preload, loading and fetchPriority
Next.js 16 deprecated priority and added preload. The docs are specific about when to use each:
| Prop | What it does | Use it when |
|---|---|---|
loading="lazy" (default) |
Waits until the image nears the viewport | Everything below the fold |
loading="eager" |
Starts loading immediately | Above-the-fold images |
fetchPriority="high" |
Raises the request priority | The single LCP image |
preload |
Adds <link rel="preload"> in the head |
The LCP image is discovered late in the body |
priority |
Deprecated since 16.0 | Replace with one of the above |
The docs say that in most cases loading="eager" or fetchPriority="high" is the better choice, and that you should not combine preload with loading or fetchPriority. Do not mark several images as high priority: if everything is important, nothing is.
LCP mistakes I see in code reviews
- The hero is lazy loaded. It is the default, so a hero without
loading="eager"waits for layout before it even starts. - The hero is a CSS
background-image. It bypassesnext/imageentirely: no resizing, no modern formats, and late discovery. Use an<Image fill>behind your content instead. - The hero renders only on the client. If it sits inside a component that waits on
useEffectdata, the image URL is not in the HTML. Render it from a Server Component. - The hero is behind a Suspense boundary that waits on request data. With Cache Components, keep LCP content in the prerendered static shell by caching its data with
'use cache'instead of streaming it. quality={90}everywhere. Since Next.js 16,images.qualitiesdefaults to[75]and other values are coerced to the closest allowed one. Add values toqualitiesonly if you need them.
Next.js font optimization with next/font
next/font downloads Google Fonts at build time and serves them from your domain. No request goes to Google from the browser. It also generates a fallback font with adjusted metrics (adjustFontFallback, on by default) so the swap from fallback to web font barely moves the layout.
// app/fonts.ts
import { Inter, Fraunces } from 'next/font/google'
import localFont from 'next/font/local'
export const inter = Inter({
subsets: ['latin'],
display: 'swap',
variable: '--font-inter',
})
export const fraunces = Fraunces({
subsets: ['latin'],
display: 'swap',
variable: '--font-fraunces',
})
export const brand = localFont({
src: './fonts/brand-variable.woff2',
display: 'swap',
variable: '--font-brand',
})// app/layout.tsx
import { inter, fraunces } from './fonts'
import './globals.css'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className={`${inter.variable} ${fraunces.variable}`}>
<body className="font-sans">{children}</body>
</html>
)
}With Tailwind CSS v4, map the variables in CSS:
/* app/globals.css */
@import 'tailwindcss';
@theme inline {
--font-sans: var(--font-inter);
--font-serif: var(--font-fraunces);
}If you are still moving from a tailwind.config.js setup, the Tailwind CSS v4 migration guide covers the @theme changes.
Font rules that keep CLS near zero
- Prefer variable fonts. One file covers every weight. Non-variable Google fonts require a
weight, and each weight is a separate file. - Always set
subsets. Preloading needs them, and Next.js warns if they are missing whilepreloadis true. - Define each font once. Every call to a font loader creates a separate instance. Put them in one
fonts.tsfile and import from there. - Load fonts where they are used. A font in the root layout is preloaded on every route. A font only used on
/blogcan be loaded in the blog layout instead. - Use fewer families. Each one is another download competing with your LCP resource.
- Consider
display: 'optional'for body text if you would rather keep the fallback than see a late swap on slow connections.
next/image and next/font defaults that changed in Next.js 16
| Setting | Before Next.js 16 | Next.js 16 |
|---|---|---|
priority prop |
Supported | Deprecated, use preload, loading or fetchPriority |
images.qualities |
All values 1 to 100 | [75] |
images.minimumCacheTTL |
60 seconds | 4 hours (14,400 seconds) |
images.imageSizes |
Included 16 |
16 removed |
images.maximumRedirects |
Unlimited | 3 |
Local src with query string |
Allowed | Requires images.localPatterns |
images.domains |
Supported | Deprecated, use remotePatterns |
next/legacy/image |
Supported | Deprecated |
If images look blurrier after upgrading, check qualities first. If a CMS image does not update for hours, the new minimumCacheTTL default is the likely reason.
Measuring the result
Lab tools like Lighthouse are useful for debugging, but Core Web Vitals are judged on field data. Check PageSpeed Insights or Search Console for real-user numbers, and use the Performance panel in Chrome DevTools to see which element is the LCP and when its request started. Next.js also exposes useReportWebVitals from next/web-vitals if you want to send metrics to your own analytics. The broader checklist is in the React performance optimization checklist.
FAQ
Is the priority prop deprecated in next/image?
Yes. Since Next.js 16, priority is deprecated in favor of preload. The docs also say that in most cases loading="eager" or fetchPriority="high" is a better choice than preload for the LCP image.
Why is my next/image LCP still slow?
The usual causes are a hero image that is lazy loaded, a missing or wrong sizes prop that downloads a huge file, a CSS background image that bypasses next/image, or a hero that only renders after client-side JavaScript runs.
Does next/font send requests to Google Fonts?
No. next/font/google downloads the CSS and font files at build time and serves them from your own domain, so the browser never contacts Google.
How do I use next/font with Tailwind CSS v4?
Give the font a variable option such as --font-inter, add font.variable to the html className, then map it in CSS with @theme inline { --font-sans: var(--font-inter); }.
What are good Core Web Vitals scores?
Google's thresholds are LCP at or under 2.5 seconds, INP at or under 200 milliseconds and CLS at or under 0.1, measured at the 75th percentile of real page loads.
Wrapping up
Most Next.js image and font problems come from a handful of defaults you did not override: the hero is lazy, sizes is missing, or fonts are loaded from a CDN link instead of next/font. Fix those, replace priority with loading="eager" or fetchPriority="high" on the one image that matters, and measure with field data. The Image component reference is worth rereading after every major upgrade, because the defaults do change.
- #nextjs
- #performance
- #core-web-vitals
- #images
- #fonts
Keep reading
Next.js8 min read
Next.js Caching Explained: use cache, cacheLife, ISR
Next.js caching explained for Next.js 16: 'use cache', cacheLife, cacheTag, revalidateTag vs updateTag vs revalidatePath, ISR and fetch caching.
Read articleNext.js10 min read
Deploy Next.js: Vercel vs Self-Hosting with Docker
Deploy Next.js on Vercel or self-host with Docker and standalone output. Compare costs, features, caching, ISR and the gotchas before you choose a host.
Read articleNext.js8 min read
Next.js App Router vs Pages Router: Migration Guide
Next.js App Router vs Pages Router explained: the real differences, when to migrate, and a step-by-step migration guide updated for Next.js 16.4.
Read article