Skip to main content
Technology & EngineeringNextjs318 lines

Image Optimization

Image optimization with next/image, asset handling, fonts, and performance tuning for Next.js applications

Quick Summary28 lines
You are an expert in optimizing images and static assets in Next.js applications using the built-in `next/image` component and related techniques.

## Key Points

- Serves images in modern formats (WebP, AVIF) based on browser support
- Resizes images on-demand and caches them
- Lazy-loads images by default (only loads when entering the viewport)
- Prevents Cumulative Layout Shift (CLS) by requiring dimensions
1. **Local images**: Imported from the filesystem; dimensions are known at build time.
2. **Remote images**: Loaded from external URLs; dimensions must be provided or `fill` must be used.
- Always use the `<Image>` component instead of raw `<img>` tags for automatic optimization.
- Set `priority` on the Largest Contentful Paint (LCP) image (hero image, above-the-fold banner).
- Provide accurate `sizes` for responsive and fill images — incorrect values lead to oversized downloads.
- Use `placeholder="blur"` for local images to improve perceived performance.
- Enable AVIF format (`formats: ["image/avif", "image/webp"]`) for the best compression ratios.
- Use `next/font` for all text fonts to eliminate font-related layout shift and external network requests.

## Quick Example

```tsx
export const metadata: Metadata = {
  openGraph: {
    images: ["/api/og?title=My+Page"],
  },
};
```
skilldb get nextjs-skills/Image OptimizationFull skill: 318 lines
Paste into your CLAUDE.md or agent config

Image Optimization — Next.js

You are an expert in optimizing images and static assets in Next.js applications using the built-in next/image component and related techniques.

Overview

Next.js provides automatic image optimization through the <Image> component from next/image. It handles lazy loading, responsive sizing, format conversion (WebP/AVIF), and caching out of the box. Combined with next/font and static asset best practices, you can achieve excellent Core Web Vitals scores.

Core Concepts

The <Image> Component

The <Image> component wraps the native <img> tag with automatic optimization:

  • Serves images in modern formats (WebP, AVIF) based on browser support
  • Resizes images on-demand and caches them
  • Lazy-loads images by default (only loads when entering the viewport)
  • Prevents Cumulative Layout Shift (CLS) by requiring dimensions

Image Sources

  1. Local images: Imported from the filesystem; dimensions are known at build time.
  2. Remote images: Loaded from external URLs; dimensions must be provided or fill must be used.

Core Philosophy

Image optimization in Next.js is built on the principle that performance should be automatic, not manual. The <Image> component handles format negotiation, responsive sizing, lazy loading, and cache management behind a single declarative API. Developers should not need to manually create WebP variants, write srcset attributes, or implement intersection observers — the framework does it for you when you use the right component.

The performance gains from proper image handling are disproportionately large. Images are typically the heaviest assets on a page, and they directly impact Largest Contentful Paint (LCP), Cumulative Layout Shift (CLS), and total page weight. Getting images right often improves Core Web Vitals more than any amount of JavaScript optimization. This is why Next.js makes the optimized path the default path: using <Image> instead of <img> is the single highest-leverage performance decision in most applications.

Font optimization follows the same philosophy of eliminating unnecessary network requests and layout instability. next/font downloads fonts at build time, self-hosts them from your domain, and applies size-adjust to prevent layout shift. The result is zero-CLS text rendering with no external font service dependency, which is both faster and more privacy-respecting than loading from Google Fonts at runtime.

Implementation Patterns

Local Images

import Image from "next/image";
import heroImage from "@/public/images/hero.jpg";

export default function Hero() {
  return (
    <Image
      src={heroImage}
      alt="Product showcase"
      placeholder="blur"       // Automatic blur-up placeholder
      priority                  // Preload for above-the-fold images
      className="rounded-lg"
    />
  );
}

With local imports, width, height, and blurDataURL are inferred automatically.

Remote Images

import Image from "next/image";

export default function UserAvatar({ user }: { user: User }) {
  return (
    <Image
      src={user.avatarUrl}
      alt={`${user.name}'s avatar`}
      width={80}
      height={80}
      className="rounded-full"
    />
  );
}

Configure allowed remote domains in next.config.ts:

// next.config.ts
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  images: {
    remotePatterns: [
      {
        protocol: "https",
        hostname: "cdn.example.com",
        pathname: "/images/**",
      },
      {
        protocol: "https",
        hostname: "avatars.githubusercontent.com",
      },
    ],
  },
};
export default nextConfig;

Fill Mode (Unknown Dimensions)

When you do not know the image dimensions ahead of time, use fill to stretch the image to its parent container:

export default function Card({ image }: { image: string }) {
  return (
    <div className="relative aspect-video w-full">
      <Image
        src={image}
        alt="Card image"
        fill
        className="object-cover rounded-md"
        sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
      />
    </div>
  );
}

Important: The parent must have position: relative and defined dimensions. The sizes attribute is critical for fill images to generate correct responsive srcset.

Responsive Images with sizes

The sizes attribute tells the browser how wide the image will be at different viewport widths so it can pick the right source from the srcset:

<Image
  src="/photos/landscape.jpg"
  alt="Landscape"
  width={1200}
  height={800}
  sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 800px"
/>

Image Configuration Options

// next.config.ts
const nextConfig: NextConfig = {
  images: {
    // Supported formats (order matters — first supported wins)
    formats: ["image/avif", "image/webp"],

    // Custom device breakpoints for srcset
    deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
    imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],

    // Cache duration in seconds (default: 60)
    minimumCacheTTL: 86400, // 24 hours

    // Use a CDN loader for external optimization
    // loader: "custom",
    // loaderFile: "./lib/image-loader.ts",
  },
};

Custom Image Loader

For external image CDNs like Cloudinary, Imgix, or Cloudflare Images:

// lib/image-loader.ts
export default function cloudinaryLoader({
  src,
  width,
  quality,
}: {
  src: string;
  width: number;
  quality?: number;
}) {
  const params = [
    `f_auto`,
    `c_limit`,
    `w_${width}`,
    `q_${quality || "auto"}`,
  ];
  return `https://res.cloudinary.com/demo/image/upload/${params.join(",")}/${src}`;
}
// next.config.ts
const nextConfig: NextConfig = {
  images: {
    loader: "custom",
    loaderFile: "./lib/image-loader.ts",
  },
};

Font Optimization with next/font

// app/layout.tsx
import { Inter, JetBrains_Mono } from "next/font/google";

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

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

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en" className={`${inter.variable} ${jetbrainsMono.variable}`}>
      <body className="font-sans">{children}</body>
    </html>
  );
}

next/font downloads fonts at build time and self-hosts them with zero layout shift.

Local Font Files

import localFont from "next/font/local";

const calSans = localFont({
  src: "./fonts/CalSans-SemiBold.woff2",
  display: "swap",
  variable: "--font-cal",
});

Open Graph Images

Generate dynamic OG images using next/og:

// app/api/og/route.tsx
import { ImageResponse } from "next/og";

export const runtime = "edge";

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const title = searchParams.get("title") ?? "My Site";

  return new ImageResponse(
    (
      <div
        style={{
          display: "flex",
          fontSize: 60,
          background: "linear-gradient(to bottom, #1e293b, #0f172a)",
          color: "white",
          width: "100%",
          height: "100%",
          alignItems: "center",
          justifyContent: "center",
          padding: 48,
        }}
      >
        {title}
      </div>
    ),
    { width: 1200, height: 630 }
  );
}

Reference it in metadata:

export const metadata: Metadata = {
  openGraph: {
    images: ["/api/og?title=My+Page"],
  },
};

Best Practices

  • Always use the <Image> component instead of raw <img> tags for automatic optimization.
  • Set priority on the Largest Contentful Paint (LCP) image (hero image, above-the-fold banner).
  • Provide accurate sizes for responsive and fill images — incorrect values lead to oversized downloads.
  • Use placeholder="blur" for local images to improve perceived performance.
  • Enable AVIF format (formats: ["image/avif", "image/webp"]) for the best compression ratios.
  • Use next/font for all text fonts to eliminate font-related layout shift and external network requests.
  • Self-host critical static assets in public/ rather than loading them from third-party CDNs.

Common Pitfalls

  • Missing sizes with fill: Without sizes, the browser may download the full-resolution image even on mobile. Always specify sizes when using fill.
  • Not configuring remotePatterns: Remote images fail without allowlisting the domain in next.config.ts. Use remotePatterns (not the deprecated domains array).
  • Forgetting priority on LCP images: Above-the-fold images are lazy-loaded by default, hurting LCP. Add priority to disable lazy loading for the hero image.
  • CLS from missing dimensions: If you omit width/height and do not use fill, the layout shifts when the image loads. Always specify dimensions or use fill with a sized parent.
  • Over-optimizing: Do not convert tiny icons or logos through the image optimizer. For small SVGs, import them directly as React components or use inline SVG.

Anti-Patterns

  • Using raw <img> tags instead of <Image> from next/image. Raw <img> tags bypass all automatic optimization — no lazy loading, no responsive sizing, no format conversion, no CLS prevention. Every image in a Next.js application should go through the <Image> component unless it is a tiny inline SVG.

  • Omitting the sizes attribute on responsive or fill images. Without sizes, the browser has no way to choose the right source from the srcset and defaults to the full-resolution image. This means mobile users download desktop-sized images, wasting bandwidth and slowing LCP. Always provide accurate sizes based on your layout breakpoints.

  • Loading hero images without the priority prop. Above-the-fold images are lazy-loaded by default, which means the browser waits until the image element enters the viewport before requesting it. For LCP images like hero banners and product photos, this delay is entirely unnecessary. Add priority to preload them.

  • Configuring remotePatterns with overly broad wildcards. Setting hostname: "**" or pathname: "/**" allows any external domain to be optimized through your server, which can be abused for bandwidth amplification attacks. Allowlist only the specific domains and paths your application actually uses.

  • Running images through the Next.js optimizer when a CDN loader would be better. If you already use Cloudinary, Imgix, or Cloudflare Images, the images are already optimized at the CDN edge. Running them through the Next.js optimizer adds latency and double-compresses them. Use a custom loader to generate CDN URLs directly.

Install this skill directly: skilldb add nextjs-skills

Get CLI access →