Skip to content

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/image prevents CLS by reserving space, but only if you give it dimensions or fill inside a sized parent.
  • Since Next.js 16, priority is deprecated. Use loading="eager" or fetchPriority="high" on the LCP image, and preload only when you need a <link> in the head.
  • Always set sizes on responsive and fill images, otherwise the browser may download a much larger file than it displays.
  • next/font self-hosts fonts and generates a metric-matched fallback to cut layout shift from font swaps.
  • Next.js 16 changed image defaults: qualities is [75], minimumCacheTTL is 4 hours, and 16 was removed from imageSizes.

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:

TSX
// 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:

TypeScript
// 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 nextConfig

With 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:

TSX
<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 bypasses next/image entirely: 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 useEffect data, 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.qualities defaults to [75] and other values are coerced to the closest allowed one. Add values to qualities only 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.

TSX
// 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',
})
TSX
// 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:

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 while preload is true.
  • Define each font once. Every call to a font loader creates a separate instance. Put them in one fonts.ts file 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 /blog can 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

Next.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 article