Images and fonts

Serve images and fonts that do not shift the layout, and stop sending a 4 MB photo to a phone.

11of 16
≈ 10Min read
7Parts

What next/image does for you

  • Resizes to the widths that are actually needed, and serves AVIF or WebP where they are supported.
  • Reserves the space from width and height, so the page never jumps when it loads.
  • Lazy-loads everything below the fold.
  • Caches the resized versions.
one component, six problems solved
import Image from "next/image";

<Image
  src="/doors/painted.jpg"
  alt="A painted door in a white frame"
  width={1200}
  height={800}
  sizes="(max-width: 768px) 100vw, 50vw"
  priority                      // only for the one above the fold
/>

The two numbers, and the aspect ratio

they are a ratio, not a size
// intrinsic: the browser knows the shape before the bytes arrive
<Image src="/door.jpg" alt="…" width={1200} height={800} />

// fill: the parent decides the box, and must be positioned
<div style={{ position: "relative", aspectRatio: "3 / 2" }}>
  <Image src="/door.jpg" alt="…" fill style={{ objectFit: "cover" }} />
</div>
This is what CLS is

An image with no reserved space pushes the text down when it loads. It is the most common cause of Cumulative Layout Shift, and the reason width and height are required unless you use fill.

sizes, and why it matters more than it looks

sizes drives the srcset selection. Getting it wrong is invisible on your laptop and expensive on someone else’s phone.

it tells the browser how wide the image will be
sizes="100vw"                                   // full width at every size
sizes="(max-width: 768px) 100vw, 50vw"          // full width on a phone, half on a desktop
sizes="(max-width: 768px) 100vw, 33vw"          // a three-column grid

// wrong sizes → the browser downloads a 2000px file for a 400px slot

priority, and the largest paint

  • One image per page gets priority: the hero, the one that is the Largest Contentful Paint.
  • Everything else stays lazy. Marking several removes the benefit.
  • placeholder="blur" gives a blurred preview: automatic for imported files, blurDataURL for remote ones.
  • Remote images need their host listed in images.remotePatterns — deliberately, so your optimiser cannot be used to resize the whole internet.

Fonts

  • The file is downloaded at build time and served from your own domain: no third-party request, no privacy question, one less connection.
  • Only the subsets you ask for are included.
  • A fallback is generated with matching metrics, so the swap barely moves anything.
  • A local file works the same way with next/font/local.
next/font — self-hosted, no request to Google
import { Space_Grotesk } from "next/font/google";

const display = Space_Grotesk({
  subsets: ["latin"],
  display: "swap",
  variable: "--font-display",
});

export default function RootLayout({ children }) {
  return (
    <html lang="en" className={display.variable}>
      <body>{children}</body>
    </html>
  );
}

display, and the flash

Two font families is a design. Five is a performance problem — each weight and style is another file.

displayBefore the font arrives
swapFallback text, swapped when ready — the usual choice
optionalFallback, and the web font is skipped if it is slow
blockInvisible text for up to 3 s — avoid
fallbackA short block, then the fallback

And the rest of it

  • alt that describes the image, or alt="" when it is decorative. Never a filename.
  • SVG for logos and icons — inline them, and skip the optimiser entirely.
  • Do not use next/image for a tiny icon; the overhead is larger than the file.
  • Check a real page in the Network panel, throttled. The number that matters is the total bytes on a phone.

Practice