- Add 100 blog posts covering AI, development, and tech topics - Add .env.example for environment configuration - Add accessibility and lighthouse audit scripts - Remove obsolete SEO reports and temporary files - Remove dev-dist build artifacts and backup files - Remove unused portrait images (moved/consolidated elsewhere) - Update contact form and component improvements Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
757 lines
20 KiB
Markdown
757 lines
20 KiB
Markdown
# 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)
|