**TL;DR.** INP auf PDP wird verbessert, indem weniger JavaScript geladen und Interaktionen optimistisch gestaltet werden. Verwenden Sie React Server Components für statische Teile. Nutzen Sie `useOptimistic` für den Variantenwechsel und das Hinzufügen zum Warenkorb. Verwenden Sie `startTransition` für nicht dringende Updates. Debouncen Sie die Suche mit `AbortController`. Heben Sie Regex hervor und vermeiden Sie Spread in Ereignishandlern. Ziel: < 200ms p75, erreichbar < 100ms.

## Was INP misst

Interaction to Next Paint (INP) misst die längste Zeit von Interaktion bis zum Malen auf einer Seite während des Besuchs des Nutzers. Eine "Interaktion" ist ein Klick, Tipp oder Tastendruck, der JavaScript auslöst.

INP wird als der **schlechteste Fall** (ohne seltene Ausreißer) berechnet, nicht als Durchschnitt. Ein langsamer Filterklick kann Ihre INP-Bewertung ruinieren, selbst wenn 99 andere Interaktionen schnell waren.

| INP-Wert        | Urteil            |
| --------------- | ----------------- |
| ≤ 200ms        | Gut               |
| 200ms – 500ms  | Verbesserungsbedarf |
| > 500ms        | Schlecht          |

## Schritt 1: Profilieren, nicht raten

Verwenden Sie den Performance-Tab in Chrome DevTools, um eine PDP-Interaktionssitzung aufzuzeichnen. Achten Sie auf:

- Lange Aufgaben (gelbe Balken > 50ms) während der Interaktionen.
- Zwangsneuladen (lila Balken).
- Hohe `attribution` für INP-Einträge.

Oder verwenden Sie die Long Animation Frames (LoAF) API in Ihrem RUM:

```ts
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.duration > 200) {
      console.log('Langer Frame:', entry);
    }
  }
});
observer.observe({ type: 'long-animation-frame', buffered: true });
```

## Schritt 2: Nicht-kritisches JS aufschieben

Die größten INP-Gewinne auf den meisten PDPs kommen von der Entfernung oder dem Aufschieben von Skripten, die Sie nicht sofort laden mussten:

| Skripttyp                  | Empfohlene Strategie           |
| -------------------------- | ------------------------------ |
| Analytik (GA4, Plausible) | `afterInteractive`            |
| Marketing-Pixel            | `afterInteractive` oder `lazyOnload` |
| Chat-Widget                | `lazyOnload` + IntersectionObserver |
| A/B-Test SDK               | Inline-blockierend (selten) oder `beforeInteractive` |
| Empfehlungs-Engine         | Server Component oder lazy IO    |
| Kundenbewertungs-Widget    | IntersectionObserver lazy load |

In Next.js:

```tsx
import Script from 'next/script';

<Script src="/marketing-pixel.js" strategy="afterInteractive" />
<Script src="/chat-widget.js" strategy="lazyOnload" />
```

## Schritt 3: In Server Components umwandeln

Die Struktur einer PDP typischerweise:

```tsx
// PDPPage (Server Component)
import { ProductGallery } from './product-gallery';      // Server
import { ProductInfo } from './product-info';            // Server
import { AddToCartButton } from './add-to-cart-button';  // Client (interaktiv)
import { VariantSelector } from './variant-selector';    // Client (interaktiv)
import { RelatedProducts } from './related-products';    // Server
import { ReviewSummary } from './review-summary';        // Server

export default async function PDP({ params }) {
  const product = await getProduct(params.slug);
  return (
    <article>
      <ProductGallery images={product.images} />
      <ProductInfo product={product} />
      <VariantSelector variants={product.variants} />
      <AddToCartButton productId={product.id} />
      <ReviewSummary reviews={product.reviews} />
      <RelatedProducts productId={product.id} />
    </article>
  );
}
```

Nur `AddToCartButton` und `VariantSelector` sind Client Components. Alles andere bleibt auf dem Server. Die Bundle-Größe sinkt um 60–80 % im Vergleich zu einer vollständig-clientseitigen PDP.

## Schritt 4: Optimistischer Variantenwechsel

Der naive Varianten-Klickfluss:

1. Nutzer klickt auf Variante.
2. POST an `/api/variant/select` mit neuer Varianten-ID.
3. Server gibt aktualisierten Preis/SKU/Bild zurück.
4. Neu-rendern mit neuem Zustand.

Insgesamt: 200–500ms wahrgenommene Latenz. INP-Wertung: schlecht.

Der optimistische Fluss:

```tsx
'use client';
import { useOptimistic, startTransition } from 'react';

export function VariantSelector({ variants, initialSelected }) {
  const [selected, setOptimistic] = useOptimistic(initialSelected);

  function handleSelect(variant) {
    setOptimistic(variant);
    startTransition(async () => {
      await selectVariantServerAction(variant.id);
    });
  }

  return (
    <div>
      {variants.map((v) => (
        <button
          key={v.id}
          onClick={() => handleSelect(v)}
          aria-pressed={selected.id === v.id}
        >
          {v.label}
        </button>
      ))}
    </div>
  );
}
```

INP sinkt auf < 100ms, da der optimistische Zustand synchron aktualisiert wird und die Serverarbeit in einer Transition läuft.

## Schritt 5: Galerie und Miniaturansicht

Die Produktgalerie ist ein häufiger INP-Killer, wenn sie alle Bilder sofort lädt. Muster:

```tsx
'use client';
import { startTransition, useState } from 'react';
import Image from 'next/image';

export function ProductGallery({ images }) {
  const [active, setActive] = useState(images[0]);

  function handleThumbClick(image) {
    startTransition(() => setActive(image));
  }

  return (
    <div>
      <Image
        src={active.src}
        width={800}
        height={800}
        priority
        alt={active.alt}
      />
      <div>
        {images.map((img) => (
          <button
            key={img.id}
            onClick={() => handleThumbClick(img)}
            onMouseEnter={() => preloadImage(img.src)}
          >
            <Image src={img.thumbSrc} width={80} height={80} alt="" />
          </button>
        ))}
      </div>
    </div>
  );
}
```

Laden Sie das überfahrene Bild vor (`onMouseEnter`), damit der Klick sofort erfolgt.

## Schritt 6: Debouncierte Suche

```tsx
'use client';
import { useEffect, useState } from 'react';

const MAX_SUGGESTIONS = 8;
const DEBOUNCE_MS = 200;

export function SearchAutocomplete() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);

  useEffect(() => {
    if (!query) {
      setResults([]);
      return;
    }
    const controller = new AbortController();
    const timeout = setTimeout(async () => {
      try {
        const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`, {
          signal: controller.signal,
        });
        const data = await res.json();
        setResults(data.slice(0, MAX_SUGGESTIONS));
      } catch (err) {
        if (err.name !== 'AbortError') throw err;
      }
    }, DEBOUNCE_MS);
    return () => {
      clearTimeout(timeout);
      controller.abort();
    };
  }, [query]);

  return (
    <>
      <input value={query} onChange={(e) => setQuery(e.target.value)} />
      <ul>
        {results.map((r) => (
          <li key={r.id}>{r.name}</li>
        ))}
      </ul>
    </>
  );
}
```

## Schritt 7: Regex hervorheben und Spread vermeiden

Schlecht:

```tsx
function handleClick(text) {
  if (/[<>]/.test(text)) { ... }   // regex wird bei jedem Klick erstellt
  setItems(prev => [...prev, ...newItems]); // spread im Akkumulator
}
```

Gut:

```tsx
const TAG_REGEX = /[<>]/;  // in den Modulbereich verschoben

function handleClick(text) {
  if (TAG_REGEX.test(text)) { ... }
  setItems(prev => prev.concat(newItems));
}
```

## Schritt 8: Lazy-Load unterhalb der Sichtbarkeit

Empfehlungen, Bewertungen, verwandte Produkte: Platzieren Sie sie unterhalb der Sichtbarkeit und laden Sie sie lazy mit `dynamic` + `ssr: false` für rein clientseitige Widgets oder laden Sie sie bei der Intersektion:

```tsx
import dynamic from 'next/dynamic';

const HeavyReviewWidget = dynamic(() => import('./heavy-review-widget'), {
  ssr: false,
  loading: () => <ReviewSkeleton />,
});
```

Für servergerenderte Abschnitte, wickeln Sie sie in `<Suspense>` mit aktiviertem PPR, damit sie nach dem statischen Shell gestreamt werden.

## Erfolg messen

Nach der Bereitstellung von Fixes zeigen INP-Verbesserungen:

1. **Chrome DevTools** unter Performance → Web Vitals Overlay (synthetisch, sofort).
2. **Ihr eigenes RUM** mit der `web-vitals`-Bibliothek (echter Nutzer, nahezu in Echtzeit).
3. **Search Console → Core Web Vitals** (echter Nutzer, p75 über 28 Tage, 2–4 Wochen Verzögerung).

Eine typische PDP, die mit 350ms p75 INP begonnen hat und dieser Anleitung folgt, erreicht innerhalb von 2–4 Wochen nach der Bereitstellung 80–120ms p75 INP.

## Wie Ordiko standardmäßig INP-gute PDPs bereitstellt

- Statische Teile der PDP sind RSC (kein clientseitiges JS geladen).
- Der Variantenwähler verwendet `useOptimistic` + Server Actions.
- Die Galerie verwendet `startTransition` + Bildvorladen beim Hover.
- Die Suche wird mit 200ms und `AbortController` debounct.
- Der Warenkorb-Drawer zeigt die zuletzt bekannte Anzahl optimistisch an.

Standard p75 INP: 80–160ms.

## FAQ

**Warum ist INP so viel schwieriger als LCP?**
LCP ist hauptsächlich ein Problem der CDN- und Bildoptimierung mit gut verstandenen Lösungen. INP ist die JavaScript-Ausführungszeit bei Benutzerinteraktionen; jeder Klick kann einen anderen Engpass haben. Die Behebung von INP erfordert Disziplin auf Komponentenebene für jedes interaktive Element auf der Seite.

**Behebt React Server Components INP automatisch?**
Im Großen und Ganzen ja. RSC reduziert das JS-Bundle, das an den Browser gesendet wird, was die Arbeit im Hauptthread verringert und INP verbessert. Aber interaktive Teile der Seite benötigen weiterhin clientseitiges JS — RSC allein behebt keine schlecht geschriebenen Client-Komponenten, die Regex in onClick erstellen.

**Was ist der größte INP-Killer auf PDPs?**
Schweres clientseitiges Rendering von Empfehlungs-Widgets ('Häufig zusammen gekauft', 'Das könnte Ihnen auch gefallen'), die 50–200KB JS liefern und aggressiv bei der ersten Interaktion hydratisieren. Verschieben Sie diese in Server Components oder laden Sie sie lazy hinter IntersectionObserver.

**Wie schnell kann eine gut optimierte PDP bei INP werden?**
Echte p75 INP unter 100ms sind erreichbar. Ordiko PDPs haben standardmäßig 80–160ms p75 INP ohne manuelle Optimierung. Benutzerdefinierte Themen, die den Mustern in dieser Anleitung folgen, können konstant unter 100ms kommen.