Menu
Nazad na Blog
2 min read
Web Development

Core 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

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

MetrikOptimierungImpact
**LCP**Image priority, preload, responsiveHoch
**LCP**Critical CSS inlineMittel
**LCP**Server-side renderingHoch
**INP**Event handler optimizationHoch
**INP**Web Workers für heavy tasksMittel
**INP**useTransition für non-blockingMittel
**CLS**Feste Dimensionen für MediaHoch
**CLS**Font display: swapMittel
**CLS**Skeleton PlaceholdersMittel

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