# 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/)
