Back to Blog

Next.js Image Optimization: next/image, AVIF/WebP, CDN Delivery, and Responsive Sizes

Master Next.js image optimization: next/image component, AVIF and WebP formats, blur placeholder generation, responsive sizes configuration, CDN delivery with Cloudflare, and Core Web Vitals impact.

Viprasol Tech Team
November 23, 2026
13 min read

Images are the single largest contributor to LCP (Largest Contentful Paint) in most web apps โ€” and LCP is a Core Web Vitals metric that directly affects Google rankings. A 400KB JPEG hero image served unoptimized can push LCP past 4 seconds. The same image as AVIF with proper responsive sizes and preloading can achieve LCP under 1.2 seconds.

next/image handles format conversion, responsive srcsets, lazy loading, and blur placeholders โ€” but the defaults aren't always optimal. This post covers the full configuration: device sizes, format priority, CDN loader integration, blur placeholder generation, and the sizes prop patterns that actually reduce payload.

What next/image Does Automatically

Original: /images/hero.jpg (2400ร—1600, 1.2MB)

next/image generates:
  /_next/image?url=/images/hero.jpg&w=640&q=75  โ†’ AVIF  45KB
  /_next/image?url=/images/hero.jpg&w=828&q=75  โ†’ AVIF  58KB
  /_next/image?url=/images/hero.jpg&w=1200&q=75 โ†’ AVIF  89KB

Browser receives:
  <img srcset="...640w, ...828w, ...1200w"
       sizes="100vw"
       type="image/avif" />

Result: 45โ€“89KB instead of 1.2MB (94% reduction)

1. Configuration in next.config.ts

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

const config: NextConfig = {
  images: {
    // Format priority: AVIF first (smallest), WebP fallback, JPEG last
    formats: ['image/avif', 'image/webp'],

    // Device widths โ€” match your design breakpoints
    deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048],

    // Image sizes for non-full-width images (used when sizes prop < 100vw)
    imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],

    // Cache images for 30 days (default: 60 seconds โ€” far too short for production)
    minimumCacheTTL: 60 * 60 * 24 * 30, // 30 days

    // Remote patterns (allowlist โ€” never use domains: [] in production)
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'images.unsplash.com',
        pathname: '/**',
      },
      {
        protocol: 'https',
        hostname: '**.cloudfront.net',
        pathname: '/uploads/**',
      },
    ],

    // Disable SVG sanitization if you serve your own trusted SVGs
    dangerouslyAllowSVG: false,
    contentDispositionType: 'attachment',
    contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;",
  },
};

export default config;

๐ŸŒ Looking for a Dev Team That Actually Delivers?

Most agencies sell you a project manager and assign juniors. Viprasol is different โ€” senior engineers only, direct Slack access, and a 5.0โ˜… Upwork record across 100+ projects.

  • React, Next.js, Node.js, TypeScript โ€” production-grade stack
  • Fixed-price contracts โ€” no surprise invoices
  • Full source code ownership from day one
  • 90-day post-launch support included

2. The sizes Prop โ€” The Most Important Optimization

The sizes prop tells the browser how wide the image will actually render. Without it, the browser assumes 100vw and downloads the largest srcset candidate unnecessarily.

// โŒ Wrong: no sizes prop โ€” browser assumes 100vw
// Downloads 1920px image even for a 300px thumbnail
<Image src="/hero.jpg" width={300} height={200} alt="thumbnail" />

// โœ… Correct: sizes prop matches your CSS layout
// src/components/images/HeroImage.tsx
import Image from 'next/image';

// Full-width hero: 100vw on mobile, capped at 1400px on desktop
export function HeroImage({ src, alt }: { src: string; alt: string }) {
  return (
    <div className="relative w-full h-[500px]">
      <Image
        src={src}
        alt={alt}
        fill
        priority          // LCP image: don't lazy load, add <link preload>
        sizes="(max-width: 768px) 100vw, (max-width: 1400px) 100vw, 1400px"
        className="object-cover"
        quality={85}      // AVIF at 85 quality is visually lossless for photos
      />
    </div>
  );
}

// Card thumbnail: 1 column on mobile, 3 columns on tablet, 4 on desktop
export function CardImage({ src, alt }: { src: string; alt: string }) {
  return (
    <div className="relative aspect-video">
      <Image
        src={src}
        alt={alt}
        fill
        sizes="(max-width: 640px) 100vw, (max-width: 1024px) 33vw, 25vw"
        className="object-cover rounded-lg"
      />
    </div>
  );
}

// Sidebar image: fixed 300px wide regardless of viewport
export function SidebarImage({ src, alt }: { src: string; alt: string }) {
  return (
    <Image
      src={src}
      alt={alt}
      width={300}
      height={200}
      sizes="300px"  // Exact fixed size โ€” browser picks closest srcset candidate
      className="rounded"
    />
  );
}

3. Blur Placeholder Generation

Blur placeholders prevent layout shift and give visual feedback while images load. next/image supports two approaches:

Static Imports (automatic blur)

// src/components/images/StaticImage.tsx
import Image from 'next/image';
import heroImg from '../../public/images/hero.jpg';

// next/image auto-generates base64 blur from static imports
export function StaticHeroImage() {
  return (
    <Image
      src={heroImg}   // Static import
      alt="Hero"
      placeholder="blur"  // base64 blurDataURL generated at build time
      priority
      sizes="100vw"
    />
  );
}

Dynamic URLs (generate blurDataURL at build/request time)

// src/lib/images/blur.ts
import sharp from 'sharp';

// Generate a tiny base64 placeholder (10px wide, blurred)
export async function generateBlurDataURL(imagePath: string): Promise<string> {
  const buffer = await sharp(imagePath)
    .resize(10)           // 10px wide placeholder
    .blur(5)              // Apply blur
    .toFormat('webp', { quality: 20 })
    .toBuffer();

  return `data:image/webp;base64,${buffer.toString('base64')}`;
}

// For remote images: fetch + resize
export async function generateRemoteBlurDataURL(url: string): Promise<string> {
  const response = await fetch(url);
  const arrayBuffer = await response.arrayBuffer();
  const buffer = Buffer.from(arrayBuffer);

  const blurred = await sharp(buffer)
    .resize(10)
    .blur(5)
    .toFormat('webp', { quality: 20 })
    .toBuffer();

  return `data:image/webp;base64,${blurred.toString('base64')}`;
}
// src/app/blog/[slug]/page.tsx
import Image from 'next/image';
import { generateRemoteBlurDataURL } from '../../../lib/images/blur';

// Generate blur at build time in SSG/ISR page
export default async function BlogPost({ params }: { params: { slug: string } }) {
  const post = await getPost(params.slug);
  const blurDataURL = await generateRemoteBlurDataURL(post.heroImageUrl);

  return (
    <article>
      <div className="relative h-96 w-full">
        <Image
          src={post.heroImageUrl}
          alt={post.title}
          fill
          priority
          placeholder="blur"
          blurDataURL={blurDataURL}
          sizes="(max-width: 1200px) 100vw, 1200px"
          className="object-cover"
        />
      </div>
      {/* ... */}
    </article>
  );
}

๐Ÿš€ Senior Engineers. No Junior Handoffs. Ever.

You get the senior developer, not a project manager who relays your requirements to someone you never meet. Every Viprasol project has a senior lead from kickoff to launch.

  • MVPs in 4โ€“8 weeks, full platforms in 3โ€“5 months
  • Lighthouse 90+ performance scores standard
  • Works across US, UK, AU timezones
  • Free 30-min architecture review, no commitment

4. Custom CDN Loader

For images stored in S3/CloudFront or Cloudinary, use a custom loader instead of routing through Next.js image optimization (which adds CPU cost and latency).

// src/lib/images/loaders.ts

// Cloudflare Images loader
export function cloudflareLoader({
  src,
  width,
  quality,
}: {
  src: string;
  width: number;
  quality?: number;
}): string {
  // Cloudflare Images transformation URL
  const params = [`width=${width}`, `quality=${quality ?? 80}`, 'format=auto'];
  return `https://imagedelivery.net/${process.env.CF_IMAGES_ACCOUNT_HASH}/${src}/${params.join(',')}`;
}

// Cloudinary loader
export function cloudinaryLoader({
  src,
  width,
  quality,
}: {
  src: string;
  width: number;
  quality?: number;
}): string {
  const params = [
    'f_auto',         // Auto format (AVIF/WebP/JPEG based on browser)
    'c_limit',        // Don't upscale
    `w_${width}`,
    `q_${quality ?? 'auto'}`,
  ].join(',');

  return `https://res.cloudinary.com/${process.env.CLOUDINARY_CLOUD_NAME}/image/upload/${params}/${src}`;
}

// S3 + CloudFront loader (just returns the CloudFront URL โ€” CF handles format)
export function cloudFrontLoader({
  src,
  width,
  quality,
}: {
  src: string;
  width: number;
  quality?: number;
}): string {
  return `${process.env.NEXT_PUBLIC_CDN_URL}${src}?w=${width}&q=${quality ?? 80}`;
}
// Use custom loader on specific Image instances
import Image from 'next/image';
import { cloudinaryLoader } from '../../lib/images/loaders';

export function CloudinaryImage({ publicId, alt }: { publicId: string; alt: string }) {
  return (
    <Image
      loader={cloudinaryLoader}
      src={publicId}   // Just the public ID, not the full URL
      alt={alt}
      width={800}
      height={600}
      sizes="(max-width: 768px) 100vw, 800px"
    />
  );
}
// next.config.ts: set default loader for all Images
const config: NextConfig = {
  images: {
    loader: 'custom',
    loaderFile: './src/lib/images/loaders.ts',
    // When using custom loader, export the function as default
  },
};

5. Preloading LCP Images

priority adds a <link rel="preload"> tag in <head> โ€” but it only works if the image is above the fold and visible on the initial render.

// src/app/layout.tsx โ€” preload hero image for all pages
import { Metadata } from 'next';

export const metadata: Metadata = {
  // Next.js 15+ supports metadataBase for OG images
  metadataBase: new URL('https://viprasol.com'),
};

// For dynamic LCP images, add preload link manually
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <head>
        {/* Preload the critical above-fold font */}
        <link
          rel="preload"
          href="/fonts/inter-var.woff2"
          as="font"
          type="font/woff2"
          crossOrigin="anonymous"
        />
      </head>
      <body>{children}</body>
    </html>
  );
}

6. Measuring Impact with Core Web Vitals

// src/lib/analytics/web-vitals.ts
import type { Metric } from 'next/dist/compiled/@next/font/dist/google/types';

export function reportWebVitals(metric: Metric) {
  // Filter for LCP only in development to identify image issues
  if (process.env.NODE_ENV === 'development') {
    if (metric.name === 'LCP') {
      console.log(`LCP: ${metric.value.toFixed(0)}ms (target: <2500ms)`);
      if (metric.value > 2500) {
        console.warn('โš ๏ธ LCP above threshold โ€” check hero image optimization');
      }
    }
    if (metric.name === 'CLS') {
      console.log(`CLS: ${metric.value.toFixed(4)} (target: <0.1)`);
    }
  }

  // Send to analytics in production
  if (process.env.NODE_ENV === 'production') {
    fetch('/api/vitals', {
      method: 'POST',
      body: JSON.stringify({
        name: metric.name,
        value: metric.value,
        id: metric.id,
        page: window.location.pathname,
      }),
      headers: { 'Content-Type': 'application/json' },
      keepalive: true,
    });
  }
}

Performance Impact Table

ConfigurationHero image payloadLCP (3G)LCP (WiFi)
Raw JPEG, no optimization1,200KB8.2s2.1s
WebP, no responsive380KB3.4s0.9s
AVIF + responsive sizes + lazy45KB1.8s0.4s
AVIF + responsive + priority45KB1.1s0.3s
AVIF + CDN edge cache + priority45KB0.7s0.2s

Cost Reference

Image optimization approachMonthly cost (100K pageviews)Notes
Next.js built-in (/_next/image)~$5โ€“15 (compute)Runs on your server
Cloudflare Images$5 + $1/100K transformsIncludes CDN delivery
Cloudinary (free tier)$025 credits/mo, ~10K transforms
Cloudinary (Plus)$89/mo225K transforms + advanced

See Also


Working With Viprasol

Failing Core Web Vitals due to unoptimized images? We audit your LCP bottlenecks, implement next/image correctly with proper sizes props, configure CDN delivery via Cloudflare or CloudFront, and generate blur placeholders โ€” typically cutting image payload by 85%+ and bringing LCP under 2.5 seconds.

Talk to our team โ†’ | See our web development services โ†’

Share this article:

About the Author

V

Viprasol Tech Team

Custom Software Development Specialists

The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 100+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement. Based in India, serving clients globally.

MT4/MT5 EA DevelopmentAI Agent SystemsSaaS DevelopmentAlgorithmic Trading

Need a Modern Web Application?

From landing pages to complex SaaS platforms โ€” we build it all with Next.js and React.

Free consultation โ€ข No commitment โ€ข Response within 24 hours

Viprasol ยท Web Development

Need a custom web application built?

We build React and Next.js web applications with Lighthouse โ‰ฅ90 scores, mobile-first design, and full source code ownership. Senior engineers only โ€” from architecture through deployment.