Menu
Back to Blog
2 min read
Performance

Image Optimization für Web

Bilder für Web optimieren. Next.js Image, AVIF/WebP, Responsive Images und Lazy Loading für beste Performance.

Image OptimizationWebPAVIFNext.js ImageResponsive ImagesLazy Loading
Image Optimization für Web

Image Optimization für Web

Meta-Description: Bilder für Web optimieren. Next.js Image, AVIF/WebP, Responsive Images und Lazy Loading für beste Performance.

Keywords: Image Optimization, WebP, AVIF, Next.js Image, Responsive Images, Lazy Loading, srcset, Performance


Einführung

Bilder machen oft 50-70% der Seitengröße aus. Mit Next.js Image Component, modernen Formaten (AVIF, WebP) und Responsive Images kann die Ladezeit drastisch reduziert werden. Dieser Guide zeigt Best Practices für optimale Bildperformance.


Image Optimization Overview

┌─────────────────────────────────────────────────────────────┐
│              IMAGE OPTIMIZATION PIPELINE                     │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  Format Comparison:                                         │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  Original PNG (1200x800):     2.4 MB                │   │
│  │  ├── JPEG (quality 80):       180 KB  (-92%)        │   │
│  │  ├── WebP (quality 80):       120 KB  (-95%)        │   │
│  │  └── AVIF (quality 70):       80 KB   (-97%)        │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  Browser Support (2026):                                    │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  AVIF:  Chrome ✓  Firefox ✓  Safari ✓  Edge ✓      │   │
│  │  WebP:  Chrome ✓  Firefox ✓  Safari ✓  Edge ✓      │   │
│  │  JPEG:  Universal ✓                                 │   │
│  │  PNG:   Universal ✓                                 │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  Responsive Strategy:                                       │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  Mobile (< 640px):    640w   - 40KB                │   │
│  │  Tablet (< 1024px):   1024w  - 80KB                │   │
│  │  Desktop (< 1920px):  1920w  - 150KB               │   │
│  │  Retina (2x):         2x scaling                    │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  Loading Strategies:                                        │
│  ├── eager:  Above-the-fold, LCP images                   │
│  ├── lazy:   Below-the-fold, defer loading                │
│  └── blur:   Placeholder während Loading                   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Next.js Image Component

// components/OptimizedImage.tsx
import Image from 'next/image';

interface OptimizedImageProps {
  src: string;
  alt: string;
  width?: number;
  height?: number;
  priority?: boolean;
  fill?: boolean;
  sizes?: string;
  className?: string;
  quality?: number;
}

export function OptimizedImage({
  src,
  alt,
  width,
  height,
  priority = false,
  fill = false,
  sizes,
  className,
  quality = 80
}: OptimizedImageProps) {
  // Blur Placeholder generieren (optional)
  const blurDataURL = `data:image/svg+xml;base64,${toBase64(shimmer(width || 700, height || 475))}`;

  return (
    <Image
      src={src}
      alt={alt}
      width={fill ? undefined : width}
      height={fill ? undefined : height}
      fill={fill}
      priority={priority}
      quality={quality}
      sizes={sizes || '(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw'}
      className={className}
      placeholder="blur"
      blurDataURL={blurDataURL}
      style={fill ? { objectFit: 'cover' } : undefined}
    />
  );
}

// Shimmer Placeholder Generator
const shimmer = (w: number, h: number) => `
<svg width="${w}" height="${h}" xmlns="http://www.w3.org/2000/svg">
  <defs>
    <linearGradient id="g">
      <stop stop-color="#f6f7f8" offset="0%"/>
      <stop stop-color="#edeef1" offset="20%"/>
      <stop stop-color="#f6f7f8" offset="40%"/>
      <stop stop-color="#f6f7f8" offset="100%"/>
    </linearGradient>
  </defs>
  <rect width="${w}" height="${h}" fill="#f6f7f8"/>
  <rect width="${w}" height="${h}" fill="url(#g)">
    <animate attributeName="x" from="-${w}" to="${w}" dur="1s" repeatCount="indefinite"/>
  </rect>
</svg>`;

const toBase64 = (str: string) =>
  typeof window === 'undefined'
    ? Buffer.from(str).toString('base64')
    : window.btoa(str);
// components/ResponsiveHero.tsx - Hero Image mit verschiedenen Breakpoints
import Image from 'next/image';

interface ResponsiveHeroProps {
  desktopSrc: string;
  mobileSrc: string;
  alt: string;
}

export function ResponsiveHero({ desktopSrc, mobileSrc, alt }: ResponsiveHeroProps) {
  return (
    <div className="relative w-full h-[50vh] md:h-[70vh]">
      {/* Mobile Image */}
      <Image
        src={mobileSrc}
        alt={alt}
        fill
        priority
        sizes="100vw"
        className="object-cover md:hidden"
      />

      {/* Desktop Image */}
      <Image
        src={desktopSrc}
        alt={alt}
        fill
        priority
        sizes="100vw"
        className="hidden md:block object-cover"
      />
    </div>
  );
}

// components/ArtDirectedImage.tsx - Art Direction mit Picture Element
export function ArtDirectedImage({
  sources,
  fallback,
  alt
}: {
  sources: Array<{ srcSet: string; media: string }>;
  fallback: string;
  alt: string;
}) {
  return (
    <picture>
      {sources.map((source, i) => (
        <source key={i} srcSet={source.srcSet} media={source.media} />
      ))}
      <img src={fallback} alt={alt} loading="lazy" />
    </picture>
  );
}

Next.js Image Configuration

// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  images: {
    // Moderne Formate (AVIF hat Priorität)
    formats: ['image/avif', 'image/webp'],

    // Device Sizes für srcset
    deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],

    // Image Sizes für kleinere Bilder (Icons, Thumbnails)
    imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],

    // Remote Patterns für externe Bilder
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'images.unsplash.com',
        pathname: '/**'
      },
      {
        protocol: 'https',
        hostname: 'cdn.example.com',
        pathname: '/images/**'
      },
      {
        protocol: 'https',
        hostname: '*.cloudinary.com'
      }
    ],

    // Minimum Cache Time (30 Tage)
    minimumCacheTTL: 60 * 60 * 24 * 30,

    // Deaktiviert static imports für Images
    disableStaticImages: false,

    // Content-Disposition Header
    contentDispositionType: 'inline',

    // Dangerously Allow SVG (Vorsicht!)
    dangerouslyAllowSVG: false,

    // Unoptimized für Static Export
    // unoptimized: true
  }
};

module.exports = nextConfig;

Image Processing with Sharp

// scripts/optimize-images.ts
import sharp from 'sharp';
import { glob } from 'glob';
import path from 'path';
import fs from 'fs/promises';

interface OptimizationOptions {
  quality: number;
  formats: ('webp' | 'avif' | 'jpeg')[];
  sizes: number[];
  outputDir: string;
}

async function optimizeImage(
  inputPath: string,
  options: OptimizationOptions
) {
  const image = sharp(inputPath);
  const metadata = await image.metadata();
  const baseName = path.basename(inputPath, path.extname(inputPath));

  const results: string[] = [];

  for (const format of options.formats) {
    for (const width of options.sizes) {
      // Skip wenn größer als Original
      if (metadata.width && width > metadata.width) continue;

      const outputName = `${baseName}-${width}w.${format}`;
      const outputPath = path.join(options.outputDir, outputName);

      let pipeline = image.clone().resize(width, null, {
        withoutEnlargement: true,
        fit: 'inside'
      });

      switch (format) {
        case 'webp':
          pipeline = pipeline.webp({
            quality: options.quality,
            effort: 6 // 0-6, higher = slower + better
          });
          break;
        case 'avif':
          pipeline = pipeline.avif({
            quality: options.quality,
            effort: 6 // 0-9
          });
          break;
        case 'jpeg':
          pipeline = pipeline.jpeg({
            quality: options.quality,
            mozjpeg: true
          });
          break;
      }

      await pipeline.toFile(outputPath);
      results.push(outputPath);
    }
  }

  return results;
}

// Batch Processing
async function batchOptimize(inputDir: string, outputDir: string) {
  const files = await glob(`${inputDir}/**/*.{jpg,jpeg,png,webp}`);

  await fs.mkdir(outputDir, { recursive: true });

  const options: OptimizationOptions = {
    quality: 80,
    formats: ['avif', 'webp', 'jpeg'],
    sizes: [640, 1024, 1920, 2560],
    outputDir
  };

  console.log(`Processing ${files.length} images...`);

  for (const file of files) {
    const results = await optimizeImage(file, options);
    console.log(`${path.basename(file)}: Generated ${results.length} variants`);
  }
}

// Blur Placeholder Generation
async function generateBlurPlaceholder(imagePath: string): Promise<string> {
  const buffer = await sharp(imagePath)
    .resize(10, 10, { fit: 'inside' })
    .blur()
    .toBuffer();

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

// LQIP (Low Quality Image Placeholder)
async function generateLQIP(imagePath: string): Promise<{
  base64: string;
  width: number;
  height: number;
}> {
  const metadata = await sharp(imagePath).metadata();

  const buffer = await sharp(imagePath)
    .resize(20, null, { fit: 'inside' })
    .jpeg({ quality: 20 })
    .toBuffer();

  return {
    base64: `data:image/jpeg;base64,${buffer.toString('base64')}`,
    width: metadata.width || 0,
    height: metadata.height || 0
  };
}

Responsive Images Pattern

// components/ResponsiveImage.tsx
'use client';

import { useState } from 'react';

interface ImageSource {
  src: string;
  width: number;
  format: 'avif' | 'webp' | 'jpeg';
}

interface ResponsiveImageProps {
  sources: ImageSource[];
  alt: string;
  sizes: string;
  priority?: boolean;
  className?: string;
  blurDataURL?: string;
}

export function ResponsiveImage({
  sources,
  alt,
  sizes,
  priority = false,
  className,
  blurDataURL
}: ResponsiveImageProps) {
  const [isLoaded, setIsLoaded] = useState(false);

  // Gruppiere nach Format
  const avifSources = sources.filter(s => s.format === 'avif');
  const webpSources = sources.filter(s => s.format === 'webp');
  const jpegSources = sources.filter(s => s.format === 'jpeg');

  const generateSrcSet = (sources: ImageSource[]) =>
    sources.map(s => `${s.src} ${s.width}w`).join(', ');

  const fallbackSrc = jpegSources[jpegSources.length - 1]?.src || '';

  return (
    <div className={`relative ${className}`}>
      {/* Blur Placeholder */}
      {blurDataURL && !isLoaded && (
        <img
          src={blurDataURL}
          alt=""
          aria-hidden="true"
          className="absolute inset-0 w-full h-full object-cover blur-lg scale-110"
        />
      )}

      <picture>
        {/* AVIF Sources */}
        {avifSources.length > 0 && (
          <source
            type="image/avif"
            srcSet={generateSrcSet(avifSources)}
            sizes={sizes}
          />
        )}

        {/* WebP Sources */}
        {webpSources.length > 0 && (
          <source
            type="image/webp"
            srcSet={generateSrcSet(webpSources)}
            sizes={sizes}
          />
        )}

        {/* JPEG Fallback */}
        <img
          src={fallbackSrc}
          srcSet={generateSrcSet(jpegSources)}
          sizes={sizes}
          alt={alt}
          loading={priority ? 'eager' : 'lazy'}
          decoding="async"
          onLoad={() => setIsLoaded(true)}
          className={`w-full h-full object-cover transition-opacity duration-300 ${
            isLoaded ? 'opacity-100' : 'opacity-0'
          }`}
        />
      </picture>
    </div>
  );
}

Lazy Loading Strategies

// components/LazyImage.tsx - Intersection Observer
'use client';

import { useState, useRef, useEffect } from 'react';

interface LazyImageProps {
  src: string;
  alt: string;
  width: number;
  height: number;
  threshold?: number;
  rootMargin?: string;
}

export function LazyImage({
  src,
  alt,
  width,
  height,
  threshold = 0.1,
  rootMargin = '100px'
}: LazyImageProps) {
  const [isInView, setIsInView] = useState(false);
  const [isLoaded, setIsLoaded] = useState(false);
  const imgRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) {
          setIsInView(true);
          observer.disconnect();
        }
      },
      { threshold, rootMargin }
    );

    if (imgRef.current) {
      observer.observe(imgRef.current);
    }

    return () => observer.disconnect();
  }, [threshold, rootMargin]);

  const aspectRatio = (height / width) * 100;

  return (
    <div
      ref={imgRef}
      className="relative overflow-hidden bg-gray-100"
      style={{ paddingBottom: `${aspectRatio}%` }}
    >
      {isInView && (
        <img
          src={src}
          alt={alt}
          onLoad={() => setIsLoaded(true)}
          className={`absolute inset-0 w-full h-full object-cover transition-opacity duration-500 ${
            isLoaded ? 'opacity-100' : 'opacity-0'
          }`}
        />
      )}

      {!isLoaded && (
        <div className="absolute inset-0 animate-pulse bg-gray-200" />
      )}
    </div>
  );
}

// hooks/useLazyImages.ts - Batch Lazy Loading
export function useLazyImages() {
  useEffect(() => {
    if ('loading' in HTMLImageElement.prototype) {
      // Native lazy loading supported
      const images = document.querySelectorAll<HTMLImageElement>('img[data-src]');
      images.forEach(img => {
        img.src = img.dataset.src!;
        img.removeAttribute('data-src');
      });
    } else {
      // Fallback to Intersection Observer
      const observer = new IntersectionObserver(
        (entries) => {
          entries.forEach(entry => {
            if (entry.isIntersecting) {
              const img = entry.target as HTMLImageElement;
              img.src = img.dataset.src!;
              img.removeAttribute('data-src');
              observer.unobserve(img);
            }
          });
        },
        { rootMargin: '50px' }
      );

      document.querySelectorAll('img[data-src]').forEach(img => {
        observer.observe(img);
      });
    }
  }, []);
}

Image Gallery with Lightbox

// components/ImageGallery.tsx
'use client';

import { useState } from 'react';
import Image from 'next/image';

interface GalleryImage {
  src: string;
  alt: string;
  width: number;
  height: number;
  thumbnail?: string;
}

interface ImageGalleryProps {
  images: GalleryImage[];
  columns?: 2 | 3 | 4;
}

export function ImageGallery({ images, columns = 3 }: ImageGalleryProps) {
  const [selectedIndex, setSelectedIndex] = useState<number | null>(null);

  const columnClass = {
    2: 'grid-cols-2',
    3: 'grid-cols-2 md:grid-cols-3',
    4: 'grid-cols-2 md:grid-cols-3 lg:grid-cols-4'
  }[columns];

  return (
    <>
      <div className={`grid ${columnClass} gap-4`}>
        {images.map((image, index) => (
          <button
            key={index}
            onClick={() => setSelectedIndex(index)}
            className="relative aspect-square overflow-hidden rounded-lg group"
          >
            <Image
              src={image.thumbnail || image.src}
              alt={image.alt}
              fill
              sizes="(max-width: 768px) 50vw, 33vw"
              className="object-cover transition-transform group-hover:scale-105"
            />
          </button>
        ))}
      </div>

      {/* Lightbox */}
      {selectedIndex !== null && (
        <Lightbox
          images={images}
          currentIndex={selectedIndex}
          onClose={() => setSelectedIndex(null)}
          onNavigate={setSelectedIndex}
        />
      )}
    </>
  );
}

// Lightbox Component
function Lightbox({
  images,
  currentIndex,
  onClose,
  onNavigate
}: {
  images: GalleryImage[];
  currentIndex: number;
  onClose: () => void;
  onNavigate: (index: number) => void;
}) {
  const current = images[currentIndex];

  const handlePrev = () => {
    onNavigate(currentIndex === 0 ? images.length - 1 : currentIndex - 1);
  };

  const handleNext = () => {
    onNavigate(currentIndex === images.length - 1 ? 0 : currentIndex + 1);
  };

  // Keyboard Navigation
  useEffect(() => {
    const handleKey = (e: KeyboardEvent) => {
      if (e.key === 'Escape') onClose();
      if (e.key === 'ArrowLeft') handlePrev();
      if (e.key === 'ArrowRight') handleNext();
    };

    window.addEventListener('keydown', handleKey);
    return () => window.removeEventListener('keydown', handleKey);
  }, [currentIndex]);

  return (
    <div
      className="fixed inset-0 z-50 bg-black/90 flex items-center justify-center"
      onClick={onClose}
    >
      <button
        onClick={(e) => { e.stopPropagation(); handlePrev(); }}
        className="absolute left-4 p-2 text-white hover:bg-white/10 rounded"
      >
        ←
      </button>

      <div
        className="relative max-w-4xl max-h-[80vh]"
        onClick={(e) => e.stopPropagation()}
      >
        <Image
          src={current.src}
          alt={current.alt}
          width={current.width}
          height={current.height}
          className="max-h-[80vh] w-auto"
          priority
        />
      </div>

      <button
        onClick={(e) => { e.stopPropagation(); handleNext(); }}
        className="absolute right-4 p-2 text-white hover:bg-white/10 rounded"
      >
        →
      </button>

      <button
        onClick={onClose}
        className="absolute top-4 right-4 p-2 text-white hover:bg-white/10 rounded"
      >
        ✕
      </button>

      <div className="absolute bottom-4 text-white">
        {currentIndex + 1} / {images.length}
      </div>
    </div>
  );
}

Format Comparison

FormatCompressionQualityBrowser SupportBest For
**AVIF**BesteExcellentModern browsersPhotos, Complex
**WebP**Sehr gutVery GoodAll modernGeneral use
**JPEG**GutGoodUniversalPhotos fallback
**PNG**KeineLosslessUniversalTransparenz
**SVG**N/AVectorUniversalIcons, Logos

Fazit

Image Optimization umfasst:

  1. Moderne Formate: AVIF/WebP mit Fallbacks
  2. Responsive Images: srcset und sizes
  3. Lazy Loading: Native oder Intersection Observer
  4. Blur Placeholders: Bessere UX während Loading

Optimierte Bilder sind der Schlüssel zu schnellen Websites.


Bildprompts

  1. "Image optimization pipeline diagram, format conversion flow"
  2. "Before and after image compression comparison, file size reduction"
  3. "Responsive images loading on different devices, mobile to desktop"

Quellen