# 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 ( {alt} ); } // Shimmer Placeholder Generator const shimmer = (w: number, h: number) => ` `; 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 (
{/* Mobile Image */} {alt} {/* Desktop Image */} {alt}
); } // 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 ( {sources.map((source, i) => ( ))} {alt} ); } ``` --- ## 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 { 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 (
{/* Blur Placeholder */} {blurDataURL && !isLoaded && ( )} {/* AVIF Sources */} {avifSources.length > 0 && ( )} {/* WebP Sources */} {webpSources.length > 0 && ( )} {/* JPEG Fallback */} {alt} setIsLoaded(true)} className={`w-full h-full object-cover transition-opacity duration-300 ${ isLoaded ? 'opacity-100' : 'opacity-0' }`} />
); } ``` --- ## 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(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 (
{isInView && ( {alt} setIsLoaded(true)} className={`absolute inset-0 w-full h-full object-cover transition-opacity duration-500 ${ isLoaded ? 'opacity-100' : 'opacity-0' }`} /> )} {!isLoaded && (
)}
); } // hooks/useLazyImages.ts - Batch Lazy Loading export function useLazyImages() { useEffect(() => { if ('loading' in HTMLImageElement.prototype) { // Native lazy loading supported const images = document.querySelectorAll('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(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 ( <>
{images.map((image, index) => ( ))}
{/* Lightbox */} {selectedIndex !== null && ( 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 (
e.stopPropagation()} > {current.alt}
{currentIndex + 1} / {images.length}
); } ``` --- ## 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)