2 min read
PerformanceImage 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
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
| 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:
- Moderne Formate: AVIF/WebP mit Fallbacks
- Responsive Images: srcset und sizes
- Lazy Loading: Native oder Intersection Observer
- Blur Placeholders: Bessere UX während Loading
Optimierte Bilder sind der Schlüssel zu schnellen Websites.
Bildprompts
- "Image optimization pipeline diagram, format conversion flow"
- "Before and after image compression comparison, file size reduction"
- "Responsive images loading on different devices, mobile to desktop"