Add blog posts, cleanup unused files, update components
- 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>
This commit is contained in:
@@ -0,0 +1,756 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user