Performance 15 Min. Lesezeit

Core Web Vitals Optimierung: INP, LCP und CLS mit Mustern aus der Praxis

Konkrete Optimierungsmuster für INP, LCP und CLS: Event-Handler-Tuning, responsive Bilder mit priority, Skeleton-Layouts gegen Layout-Shifts und Lighthouse-Profiling.

Seit März 2024 ist Interaction to Next Paint (INP) offiziell ein Core Web Vital — First Input Delay (FID) wurde ersetzt. Das ändert die Prioritätensetzung bei Performance-Audits erheblich. Dieser Artikel zeigt konkrete Optimierungsmuster für alle drei aktuellen Core Web Vitals: INP, LCP und CLS.

INP: Interaction to Next Paint

INP misst die Reaktionszeit auf Nutzerinteraktionen über den gesamten Lebenszyklus einer Seite — nicht nur den ersten Klick. Der Schwellenwert: unter 200ms gilt als “gut”, über 500ms als “schlecht”.

Hauptursachen für schlechtes INP

1. Langer Haupt-Thread durch synchronen JavaScript-Code

// Problematisch: synchrone Berechnung blockiert den Haupt-Thread
button.addEventListener('click', () => {
  const result = expensiveCalculation(largeDataset); // blockiert für 300ms
  updateUI(result);
});

// Besser: Arbeit in Tasks aufteilen
button.addEventListener('click', async () => {
  // Lass den Browser zunächst die visuelle Reaktion rendern
  const result = await scheduler.postTask(() => expensiveCalculation(largeDataset), {
    priority: 'background',
  });
  updateUI(result);
});

2. Unnötige Reflows durch DOM-Manipulationen

// Falsch: abwechselnd lesen und schreiben erzwingt mehrere Reflows
elements.forEach((el) => {
  const height = el.offsetHeight; // Lesen (Browser muss Layout berechnen)
  el.style.height = height * 2 + 'px'; // Schreiben
  const width = el.offsetWidth; // Lesen (erzwingt erneutes Layout)
  el.style.width = width + 'px'; // Schreiben
});

// Richtig: alle Lesezugriffe erst, dann alle Schreibzugriffe
const measurements = elements.map((el) => ({
  height: el.offsetHeight,
  width: el.offsetWidth,
}));
elements.forEach((el, i) => {
  el.style.height = measurements[i].height * 2 + 'px';
  el.style.width = measurements[i].width + 'px';
});

3. Event-Handler ohne Debouncing auf hochfrequenten Events

// Scroll-Handler ohne Throttling — feuert hunderte Male pro Sekunde
window.addEventListener('scroll', updateStickyHeader);

// Mit requestAnimationFrame throttlen
let rafPending = false;
window.addEventListener('scroll', () => {
  if (rafPending) return;
  rafPending = true;
  requestAnimationFrame(() => {
    updateStickyHeader();
    rafPending = false;
  });
});

INP in React-Inseln optimieren

import { startTransition, useState } from 'react';

export function SearchFilter({ onFilter }) {
  const [query, setQuery] = useState('');

  function handleInput(e) {
    const val = e.target.value;
    // Input-Update ist dringend (hohe Priorität)
    setQuery(val);
    // Filter-Berechnung ist nicht dringend (niedrige Priorität)
    startTransition(() => {
      onFilter(val);
    });
  }

  return <input value={query} onChange={handleInput} />;
}

startTransition markiert State-Updates als nicht dringend. React kann sie unterbrechen und zurückstellen, wenn dringendere Arbeit (wie Tastatureingaben) wartet.

LCP: Largest Contentful Paint

LCP misst, wann das größte sichtbare Inhaltselement geladen ist. Unter 2,5 Sekunden ist “gut”. Das LCP-Element ist meistens ein Bild — entweder das Hero-Bild oder ein großes Produkt-Bild above the fold.

Bild-Priorisierung in Astro

---
import { Image } from 'astro:assets';
import heroBild from '../assets/images/hero-wender-media.webp';
---

<!-- fetchpriority="high" + loading="eager" für das LCP-Bild -->
<Image
  src={heroBild}
  alt="Webdesign und SEO von Wender Media — Halle (Saale)"
  width={1200}
  height={600}
  fetchpriority="high"
  loading="eager"
  decoding="async"
  quality={85}
/>

Das fetchpriority="high"-Attribut weist den Browser an, dieses Bild frühzeitig in der Fetch-Queue zu priorisieren — noch bevor das Preload-Scanner es entdeckt.

Preload für kritische Ressourcen

---
// Im <head> des Layouts
---
<link
  rel="preload"
  href="/fonts/inter-var.woff2"
  as="font"
  type="font/woff2"
  crossorigin
/>

<!-- Preload für das LCP-Bild, falls es als CSS-Background kommt -->
<link
  rel="preload"
  fetchpriority="high"
  as="image"
  href="/images/hero-bg.webp"
  type="image/webp"
/>

Responsive Images mit srcset

<!-- Astro's Image-Komponente generiert srcset automatisch -->
<Image
  src={heroImage}
  alt="..."
  widths={[400, 800, 1200, 1600]}
  sizes="(max-width: 768px) 100vw, (max-width: 1200px) 80vw, 1200px"
  formats={['avif', 'webp']}
/>

AVIF bietet ca. 50% kleinere Dateigrößen gegenüber WebP bei gleicher Qualität — mit automatischem Fallback auf WebP für ältere Browser.

CLS: Cumulative Layout Shift

CLS misst unerwartete Verschiebungen im Layout während des Ladens. Unter 0,1 ist “gut”. Layout Shifts zerstören die Nutzererfahrung — besonders mobil, wenn Nutzer gerade auf einen Link klicken wollen, der sich verschiebt.

Häufigste Ursachen und Lösungen

1. Bilder ohne explizite Dimensionen

<!-- FEHLER: kein width/height → Browser reserviert keinen Platz -->
<img src="/images/team.jpg" alt="..." />

<!-- RICHTIG: explizite Dimensionen oder aspect-ratio -->
<img
  src="/images/team.jpg"
  alt="..."
  width="800"
  height="450"
/>

<!-- Oder via CSS aspect-ratio -->
<img
  src="/images/team.jpg"
  alt="..."
  class="w-full aspect-video object-cover"
/>

2. Dynamisch eingefügte Elemente above the fold

Cookie-Banner, Newsletter-Popups oder Ads, die nach dem initialen Render eingefügt werden, verschieben existierende Inhalte. Lösung: Platz reservieren.

/* Reservierter Platz für den Cookie-Banner */
.cookie-banner-placeholder {
  min-height: 80px; /* Approximate height des Banners */
}

/* Oder: Banner positioniert absolut über den Content */
.cookie-banner {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  /* Kein Layout Shift, weil fixed */
}

3. Skeleton-Layouts für dynamische Inhalte

---
// Skeleton-Komponente für Lade-Zustände
---
<div class="grid grid-cols-1 md:grid-cols-3 gap-6" aria-busy="true" aria-label="Artikel werden geladen">
  {Array.from({ length: 3 }).map((_, i) => (
    <div class="bg-gray-100 dark:bg-gray-800 rounded-xl p-6 animate-pulse" aria-hidden="true">
      <div class="h-4 bg-gray-200 dark:bg-gray-700 rounded w-1/3 mb-4"></div>
      <div class="h-6 bg-gray-200 dark:bg-gray-700 rounded w-full mb-2"></div>
      <div class="h-6 bg-gray-200 dark:bg-gray-700 rounded w-4/5 mb-4"></div>
      <div class="h-4 bg-gray-200 dark:bg-gray-700 rounded w-1/2"></div>
    </div>
  ))}
</div>

Das animate-pulse von Tailwind erzeugt eine Puls-Animation, die signalisiert, dass Inhalte geladen werden — ohne tatsächlichen Layout Shift.

4. Web Fonts und FOUT

Font-Swap erzeugt einen Layout Shift, wenn der Fallback-Font eine andere Metrik hat als der Web Font:

/* font-face mit font-display: optional — vermeidet FOUT komplett */
@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter-var.woff2') format('woff2');
  font-display: optional;
  font-weight: 100 900;
}

font-display: optional lädt den Font nur, wenn er innerhalb eines sehr kurzen Zeitfensters verfügbar ist — vermeidet FOUT vollständig.

Lighthouse-Profiling: praxisnah

Lighthouse im Chrome DevTools liefert einen CWV-Score, aber keine Root-Cause-Analyse. Dafür braucht man das Performance-Panel.

Long Task Detection

  1. Chrome DevTools öffnen → Performance Tab
  2. Aufzeichnung starten → repräsentative Interaktion ausführen
  3. Flame Chart nach roten Balken suchen (Long Tasks > 50ms)
  4. Den spezifischen Stack-Frame identifizieren
// PerformanceObserver für INP in der Produktion
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.entryType === 'event' && entry.duration > 100) {
      console.warn('Langsame Interaktion:', {
        type: entry.name,
        duration: entry.duration,
        target: entry.target,
      });
    }
  }
});
observer.observe({ type: 'event', buffered: true, durationThreshold: 100 });

Feld-Daten vs. Lab-Daten

Lighthouse ist ein Lab-Tool — es misst unter kontrollierten Bedingungen. Die echten CWV-Werte kommen aus dem Chrome User Experience Report (CrUX) und werden in der Google Search Console unter “Core Web Vitals” angezeigt.

Feld-Daten (CrUX) und Lab-Daten (Lighthouse) können stark voneinander abweichen — besonders INP, weil es reale Nutzerinteraktionen misst, die Lighthouse nicht simulieren kann.

Weiterführende Artikel


Performance-Audit für Ihre Website? Wender Media identifiziert Bottlenecks und behebt sie — messbar und nachhaltig. Schreiben Sie uns: info@wendermedia.info.

Individuelle Beratung gewünscht?

Wender Media unterstützt Sie bei der praktischen Umsetzung — von der technischen Konzeption bis zum Launch. Schreiben Sie uns, wir antworten innerhalb von 24 Stunden.

Jetzt Beratung anfragen

Kostenlos & unverbindlich — info@wendermedia.info

Verwandte Artikel