# 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

```tsx
// 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);
```

```tsx
// 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

```javascript
// 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

```typescript
// 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

```tsx
// 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

```tsx
// 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

```tsx
// 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

| Format | Compression | Quality | Browser Support | Best For |
|--------|-------------|---------|-----------------|----------|
| **AVIF** | Beste | Excellent | Modern browsers | Photos, Complex |
| **WebP** | Sehr gut | Very Good | All modern | General use |
| **JPEG** | Gut | Good | Universal | Photos fallback |
| **PNG** | Keine | Lossless | Universal | Transparenz |
| **SVG** | N/A | Vector | Universal | Icons, 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

- [Next.js Image Optimization](https://nextjs.org/docs/app/building-your-application/optimizing/images)
- [Web.dev Image Optimization](https://web.dev/fast/use-imagemin-to-compress-images)
- [Sharp Documentation](https://sharp.pixelplumbing.com/)
- [MDN Responsive Images](https://developer.mozilla.org/en-US/docs/Learn/HTML/Multimedia_and_embedding/Responsive_images)
