Files
Portfolio/blog-posts/75-core-web-vitals-optimization.md
T
damjan_savicandClaude Opus 4.5 e1bbe5455d Initial commit: Portfolio Website
Vollständige Next.js 15 Portfolio-Website mit:
- Blog-System mit 100+ Artikeln
- Supabase-Integration
- Responsive Design mit Tailwind CSS
- TypeScript-Konfiguration
- Testing-Setup mit Vitest und Playwright

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 15:07:20 +01:00

695 lines
19 KiB
Markdown

# Core Web Vitals Optimization
**Meta-Description:** Core Web Vitals 2026 optimieren. LCP, INP, CLS verbessern mit praktischen Strategien für Next.js und React Anwendungen.
**Keywords:** Core Web Vitals, LCP, INP, CLS, Web Performance, Page Speed, User Experience, Next.js Performance
---
## Einführung
**Core Web Vitals** sind Googles wichtigste Metriken für User Experience. Mit dem **INP Update 2024** (ersetzt FID) und neuen **2026 Benchmarks** ist Performance-Optimierung wichtiger denn je. Dieser Guide zeigt praktische Strategien für moderne Web-Apps.
---
## Core Web Vitals Overview
```
┌─────────────────────────────────────────────────────────────┐
│ CORE WEB VITALS 2026 │
├─────────────────────────────────────────────────────────────┤
│ │
│ LCP (Largest Contentful Paint): │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Loading Performance │ │
│ │ ├── Good: ≤ 2.5s │ │
│ │ ├── Needs Improvement: 2.5s - 4.0s │ │
│ │ └── Poor: > 4.0s │ │
│ │ │ │
│ │ Typical LCP Elements: │ │
│ │ ├── <img> (Hero Images) │ │
│ │ ├── <video> poster │ │
│ │ ├── Background images (CSS) │ │
│ │ └── Block-level text elements │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ INP (Interaction to Next Paint): │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Interactivity (replaced FID in 2024) │ │
│ │ ├── Good: ≤ 200ms │ │
│ │ ├── Needs Improvement: 200ms - 500ms │ │
│ │ └── Poor: > 500ms │ │
│ │ │ │
│ │ Measured Interactions: │ │
│ │ ├── Click / Tap │ │
│ │ ├── Keyboard input │ │
│ │ └── Touch interactions │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ CLS (Cumulative Layout Shift): │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Visual Stability │ │
│ │ ├── Good: ≤ 0.1 │ │
│ │ ├── Needs Improvement: 0.1 - 0.25 │ │
│ │ └── Poor: > 0.25 │ │
│ │ │ │
│ │ Common Causes: │ │
│ │ ├── Images without dimensions │ │
│ │ ├── Ads, embeds, iframes │ │
│ │ ├── Dynamic content injection │ │
│ │ └── Web fonts (FOUT/FOIT) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
```
---
## LCP Optimization
```typescript
// next.config.js - Image Optimization
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
// Moderne Formate priorisieren
formats: ['image/avif', 'image/webp'],
// Device Sizes für responsive images
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048],
// Image Sizes für kleinere Bilder
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
// Remote Patterns
remotePatterns: [
{
protocol: 'https',
hostname: 'images.example.com',
pathname: '/**'
}
],
// Minimale Cache-Zeit
minimumCacheTTL: 60 * 60 * 24 * 30 // 30 Tage
}
};
module.exports = nextConfig;
```
```tsx
// components/HeroImage.tsx - Optimized LCP Element
import Image from 'next/image';
interface HeroImageProps {
src: string;
alt: string;
}
export function HeroImage({ src, alt }: HeroImageProps) {
return (
<div className="relative w-full h-[60vh]">
<Image
src={src}
alt={alt}
fill
priority // Kritisch für LCP!
sizes="100vw"
style={{ objectFit: 'cover' }}
// Placeholder für bessere UX
placeholder="blur"
blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRg..."
/>
</div>
);
}
// Preload kritischer Ressourcen
// app/layout.tsx
export default function RootLayout({ children }) {
return (
<html>
<head>
{/* LCP Image Preload */}
<link
rel="preload"
as="image"
href="/hero-image.webp"
imageSrcSet="/hero-640.webp 640w, /hero-1200.webp 1200w"
imageSizes="100vw"
/>
{/* Critical Font Preload */}
<link
rel="preload"
as="font"
type="font/woff2"
href="/fonts/inter-var.woff2"
crossOrigin="anonymous"
/>
</head>
<body>{children}</body>
</html>
);
}
```
```typescript
// lib/critical-css.ts - Critical CSS Extraction
import { PurgeCSS } from 'purgecss';
import critical from 'critical';
async function generateCriticalCSS(html: string, css: string): Promise<string> {
const result = await critical.generate({
html,
css,
width: 1300,
height: 900,
inline: false,
extract: true,
penthouse: {
blockJSRequests: false
}
});
return result.css;
}
// Inline Critical CSS
export function InlineCriticalCSS({ css }: { css: string }) {
return (
<style
dangerouslySetInnerHTML={{ __html: css }}
data-critical="true"
/>
);
}
```
---
## INP Optimization
```typescript
// hooks/useOptimizedCallback.ts - Debounced Interactions
import { useCallback, useRef } from 'react';
export function useOptimizedCallback<T extends (...args: any[]) => void>(
callback: T,
delay: number = 100
) {
const timeoutRef = useRef<NodeJS.Timeout>();
const lastCallRef = useRef<number>(0);
return useCallback((...args: Parameters<T>) => {
const now = Date.now();
// Sofortige Ausführung wenn genug Zeit vergangen
if (now - lastCallRef.current >= delay) {
lastCallRef.current = now;
callback(...args);
return;
}
// Debounce für schnelle Aufrufe
clearTimeout(timeoutRef.current);
timeoutRef.current = setTimeout(() => {
lastCallRef.current = Date.now();
callback(...args);
}, delay - (now - lastCallRef.current));
}, [callback, delay]);
}
// hooks/useIdleCallback.ts - Non-blocking Operations
export function useIdleCallback() {
const scheduleIdleTask = useCallback((
task: () => void,
options?: IdleRequestOptions
) => {
if ('requestIdleCallback' in window) {
return requestIdleCallback(() => task(), options);
}
// Fallback
return setTimeout(task, 1) as unknown as number;
}, []);
const cancelIdleTask = useCallback((id: number) => {
if ('cancelIdleCallback' in window) {
cancelIdleCallback(id);
} else {
clearTimeout(id);
}
}, []);
return { scheduleIdleTask, cancelIdleTask };
}
```
```tsx
// components/OptimizedList.tsx - Virtualized List für große Datenmengen
'use client';
import { useVirtualizer } from '@tanstack/react-virtual';
import { useRef, useCallback, useTransition } from 'react';
interface Item {
id: string;
title: string;
description: string;
}
interface OptimizedListProps {
items: Item[];
onItemClick: (item: Item) => void;
}
export function OptimizedList({ items, onItemClick }: OptimizedListProps) {
const parentRef = useRef<HTMLDivElement>(null);
const [isPending, startTransition] = useTransition();
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 80, // Geschätzte Item-Höhe
overscan: 5 // Extra Items für smooth scrolling
});
// Non-blocking click handler
const handleClick = useCallback((item: Item) => {
// Sofortiges visuelles Feedback
startTransition(() => {
onItemClick(item);
});
}, [onItemClick]);
return (
<div
ref={parentRef}
className="h-[600px] overflow-auto"
style={{ contain: 'strict' }} // CSS Containment für Performance
>
<div
style={{
height: `${virtualizer.getTotalSize()}px`,
width: '100%',
position: 'relative'
}}
>
{virtualizer.getVirtualItems().map((virtualItem) => {
const item = items[virtualItem.index];
return (
<div
key={item.id}
onClick={() => handleClick(item)}
className="absolute top-0 left-0 w-full p-4 border-b cursor-pointer hover:bg-gray-50"
style={{
height: `${virtualItem.size}px`,
transform: `translateY(${virtualItem.start}px)`,
// Vermeidet Layout Thrashing
contain: 'layout style paint'
}}
>
<h3 className="font-semibold">{item.title}</h3>
<p className="text-gray-600 truncate">{item.description}</p>
</div>
);
})}
</div>
{isPending && (
<div className="absolute top-2 right-2">
<span className="text-sm text-gray-500">Loading...</span>
</div>
)}
</div>
);
}
```
```typescript
// lib/web-worker.ts - Offload Heavy Computation
// workers/calculation.worker.ts
self.onmessage = (event: MessageEvent<{ data: number[] }>) => {
const { data } = event.data;
// Schwere Berechnung im Worker
const result = data.reduce((sum, val) => {
// Simuliere komplexe Berechnung
for (let i = 0; i < 1000; i++) {
sum += Math.sqrt(val * i);
}
return sum;
}, 0);
self.postMessage({ result });
};
// hooks/useWebWorker.ts
import { useEffect, useRef, useCallback, useState } from 'react';
export function useCalculationWorker() {
const workerRef = useRef<Worker>();
const [result, setResult] = useState<number | null>(null);
const [isCalculating, setIsCalculating] = useState(false);
useEffect(() => {
workerRef.current = new Worker(
new URL('../workers/calculation.worker.ts', import.meta.url)
);
workerRef.current.onmessage = (event) => {
setResult(event.data.result);
setIsCalculating(false);
};
return () => workerRef.current?.terminate();
}, []);
const calculate = useCallback((data: number[]) => {
setIsCalculating(true);
workerRef.current?.postMessage({ data });
}, []);
return { calculate, result, isCalculating };
}
```
---
## CLS Optimization
```tsx
// components/ResponsiveImage.tsx - Prevent Layout Shift
import Image from 'next/image';
interface ResponsiveImageProps {
src: string;
alt: string;
width: number;
height: number;
}
export function ResponsiveImage({ src, alt, width, height }: ResponsiveImageProps) {
// Aspect Ratio für Layout Reservierung
const aspectRatio = (height / width) * 100;
return (
<div
className="relative w-full"
style={{ paddingBottom: `${aspectRatio}%` }}
>
<Image
src={src}
alt={alt}
fill
sizes="(max-width: 768px) 100vw, 50vw"
style={{ objectFit: 'cover' }}
/>
</div>
);
}
// components/Skeleton.tsx - Content Placeholders
interface SkeletonProps {
className?: string;
variant?: 'text' | 'rectangular' | 'circular';
width?: string | number;
height?: string | number;
}
export function Skeleton({
className = '',
variant = 'rectangular',
width,
height
}: SkeletonProps) {
const baseStyles = 'animate-pulse bg-gray-200';
const variantStyles = {
text: 'rounded',
rectangular: 'rounded-md',
circular: 'rounded-full'
};
return (
<div
className={`${baseStyles} ${variantStyles[variant]} ${className}`}
style={{ width, height }}
aria-hidden="true"
/>
);
}
// Verwendung für Layout Stability
export function ArticleCardSkeleton() {
return (
<div className="p-4 border rounded-lg">
<Skeleton variant="rectangular" height={200} className="mb-4" />
<Skeleton variant="text" height={24} className="mb-2" />
<Skeleton variant="text" height={16} width="60%" />
</div>
);
}
```
```tsx
// components/FontOptimization.tsx - Prevent FOUT/FOIT
// next.config.js
const nextConfig = {
optimizeFonts: true
};
// app/layout.tsx - Font Preloading
import { Inter, Roboto_Mono } from 'next/font/google';
const inter = Inter({
subsets: ['latin'],
display: 'swap', // Verhindert FOIT
variable: '--font-inter',
preload: true,
fallback: ['system-ui', 'arial']
});
const robotoMono = Roboto_Mono({
subsets: ['latin'],
display: 'swap',
variable: '--font-roboto-mono',
preload: true
});
export default function RootLayout({ children }) {
return (
<html lang="de" className={`${inter.variable} ${robotoMono.variable}`}>
<body className="font-sans">{children}</body>
</html>
);
}
// CSS Variable Usage
// globals.css
/*
:root {
--font-sans: var(--font-inter), system-ui, sans-serif;
--font-mono: var(--font-roboto-mono), monospace;
}
body {
font-family: var(--font-sans);
/* Font-size-adjust verhindert Layout Shift */
font-size-adjust: 0.5;
}
*/
```
```tsx
// components/DynamicContent.tsx - Reservierter Platz
'use client';
import { useState, useEffect } from 'react';
interface DynamicContentProps {
fetchContent: () => Promise<string>;
minHeight: number; // Minimale Höhe reservieren
}
export function DynamicContent({ fetchContent, minHeight }: DynamicContentProps) {
const [content, setContent] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
fetchContent()
.then(setContent)
.finally(() => setIsLoading(false));
}, [fetchContent]);
return (
<div
style={{
minHeight,
// CSS Containment für Layout Isolation
contain: 'layout'
}}
>
{isLoading ? (
<Skeleton height={minHeight} />
) : (
<div dangerouslySetInnerHTML={{ __html: content || '' }} />
)}
</div>
);
}
// Ad Container mit reserviertem Platz
export function AdContainer({
width,
height
}: {
width: number;
height: number
}) {
return (
<div
style={{
width,
height,
minWidth: width,
minHeight: height,
contain: 'strict',
backgroundColor: '#f0f0f0'
}}
data-ad-slot="123456"
>
{/* Ad loads here */}
</div>
);
}
```
---
## Performance Monitoring
```typescript
// lib/web-vitals.ts - Real User Monitoring
import { onLCP, onINP, onCLS, Metric } from 'web-vitals';
interface VitalsReport {
metric: string;
value: number;
rating: 'good' | 'needs-improvement' | 'poor';
navigationType: string;
url: string;
}
function sendToAnalytics(report: VitalsReport) {
// An Analytics senden
fetch('/api/vitals', {
method: 'POST',
body: JSON.stringify(report),
headers: { 'Content-Type': 'application/json' }
});
// Oder an Google Analytics
if (typeof gtag !== 'undefined') {
gtag('event', report.metric, {
value: Math.round(report.value),
event_category: 'Web Vitals',
event_label: report.rating,
non_interaction: true
});
}
}
function getRating(metric: Metric): 'good' | 'needs-improvement' | 'poor' {
const thresholds = {
LCP: [2500, 4000],
INP: [200, 500],
CLS: [0.1, 0.25]
};
const [good, poor] = thresholds[metric.name as keyof typeof thresholds] || [0, 0];
if (metric.value <= good) return 'good';
if (metric.value <= poor) return 'needs-improvement';
return 'poor';
}
export function initWebVitals() {
const reportMetric = (metric: Metric) => {
sendToAnalytics({
metric: metric.name,
value: metric.value,
rating: getRating(metric),
navigationType: metric.navigationType || 'unknown',
url: window.location.href
});
};
onLCP(reportMetric);
onINP(reportMetric);
onCLS(reportMetric);
}
// app/providers.tsx
'use client';
import { useEffect } from 'react';
import { initWebVitals } from '@/lib/web-vitals';
export function WebVitalsProvider({ children }: { children: React.ReactNode }) {
useEffect(() => {
initWebVitals();
}, []);
return <>{children}</>;
}
```
---
## Optimization Checklist
| Metrik | Optimierung | Impact |
|--------|-------------|--------|
| **LCP** | Image priority, preload, responsive | Hoch |
| **LCP** | Critical CSS inline | Mittel |
| **LCP** | Server-side rendering | Hoch |
| **INP** | Event handler optimization | Hoch |
| **INP** | Web Workers für heavy tasks | Mittel |
| **INP** | useTransition für non-blocking | Mittel |
| **CLS** | Feste Dimensionen für Media | Hoch |
| **CLS** | Font display: swap | Mittel |
| **CLS** | Skeleton Placeholders | Mittel |
---
## Fazit
Core Web Vitals Optimization erfordert:
1. **LCP**: Prioritize above-fold content, optimize images
2. **INP**: Non-blocking interactions, Web Workers
3. **CLS**: Reserved space, stable layouts
4. **Monitoring**: Real User Monitoring für kontinuierliche Verbesserung
Performance ist kein Feature, sondern Grundvoraussetzung.
---
## Bildprompts
1. "Core Web Vitals dashboard showing green metrics, performance monitoring"
2. "Page loading timeline visualization, LCP element highlighted"
3. "Layout shift animation showing before and after optimization"
---
## Quellen
- [Web.dev Core Web Vitals](https://web.dev/vitals/)
- [Google Search Central Page Experience](https://developers.google.com/search/docs/appearance/page-experience)
- [Next.js Performance Optimization](https://nextjs.org/docs/app/building-your-application/optimizing)
- [Chrome DevTools Performance](https://developer.chrome.com/docs/devtools/performance/)