2 min read
Web DevelopmentCore Web Vitals Optimization
Core Web Vitals 2026 optimieren. LCP, INP, CLS verbessern mit praktischen Strategien für Next.js und React Anwendungen.
Core Web VitalsLCPINPCLSWeb PerformancePage Speed

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
// 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;// 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>
);
}// 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
// 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 };
}// 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>
);
}// 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
// 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>
);
}// 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;
}
*/// 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
// 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:
- LCP: Prioritize above-fold content, optimize images
- INP: Non-blocking interactions, Web Workers
- CLS: Reserved space, stable layouts
- Monitoring: Real User Monitoring für kontinuierliche Verbesserung
Performance ist kein Feature, sondern Grundvoraussetzung.
Bildprompts
- "Core Web Vitals dashboard showing green metrics, performance monitoring"
- "Page loading timeline visualization, LCP element highlighted"
- "Layout shift animation showing before and after optimization"