**TL;DR.** L'INP sulla PDP è migliorato riducendo il JavaScript e rendendo le interazioni ottimistiche. Usa i React Server Components per le parti statiche. Usa `useOptimistic` per il cambio di variante e l'aggiunta al carrello. Usa `startTransition` per aggiornamenti non urgenti. Debounce la ricerca con `AbortController`. Solleva le regex ed evita lo spread nei gestori di eventi. Obiettivo: < 200ms p75, raggiungibile < 100ms.

## Cosa misura l'INP

L'Interaction to Next Paint (INP) misura il tempo di interazione al prossimo paint più lungo su una pagina durante la visita dell'utente. Un'"interazione" è un clic, un tocco o una pressione di tasto che attiva JavaScript.

L'INP è calcolato come il **peggiore caso** (escludendo rari outlier), non la media. Un clic lento su un filtro può rovinare il tuo punteggio INP anche se 99 altre interazioni sono state veloci.

| Valore INP      | Giudizio           |
| --------------- | ------------------ |
| ≤ 200ms         | Buono              |
| 200ms – 500ms   | Necessita miglioramenti |
| > 500ms         | Scarso             |

## Passo 1: Profilare, non indovinare

Usa la scheda Performance di Chrome DevTools per registrare una sessione di interazione PDP. Cerca:

- Compiti lunghi (barre gialle > 50ms) durante le interazioni.
- Riflussi forzati (barre viola).
- Pesante `attribution` per le voci INP.

Oppure usa l'API Long Animation Frames (LoAF) nel tuo RUM:

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

## Passo 2: Differire il JS non critico

I maggiori guadagni INP nella maggior parte delle PDP derivano dalla rimozione o dal differimento degli script che non era necessario caricare in modo anticipato:

| Tipo di script             | Strategia raccomandata           |
| -------------------------- | -------------------------------- |
| Analytics (GA4, Plausible) | `afterInteractive`               |
| Pixel di marketing         | `afterInteractive` o `lazyOnload` |
| Widget di chat             | `lazyOnload` + IntersectionObserver |
| SDK di test A/B           | Inline blocking (raro) o `beforeInteractive` |
| Motore di raccomandazione  | Server Component o lazy IO       |
| Widget di recensioni       | Caricamento lazy con IntersectionObserver |

In Next.js:

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

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

## Passo 3: Convertire in Server Components

Struttura tipica della PDP:

```tsx
// PDPPage (Server Component)
import { ProductGallery } from './product-gallery';      // Server
import { ProductInfo } from './product-info';            // Server
import { AddToCartButton } from './add-to-cart-button';  // Client (interattivo)
import { VariantSelector } from './variant-selector';    // Client (interattivo)
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>
  );
}
```

Solo `AddToCartButton` e `VariantSelector` sono Client Components. Tutto il resto rimane sul server. La dimensione del bundle diminuisce del 60–80% rispetto a una PDP completamente client.

## Passo 4: Cambio di variante ottimistico

Il flusso di clic sulla variante naive:

1. L'utente clicca sulla variante.
2. POST a `/api/variant/select` con il nuovo ID variante.
3. Il server restituisce prezzo/SKU/immagine aggiornati.
4. Rende di nuovo con il nuovo stato.

Totale: 200–500ms di latenza percepita. Punteggio INP: scarso.

Il flusso ottimistico:

```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>
  );
}
```

L'INP scende a < 100ms perché gli aggiornamenti dello stato ottimistico avvengono in modo sincrono e il lavoro del server viene eseguito in una transizione.

## Passo 5: Galleria e miniatura

La galleria dei prodotti è un comune killer di INP se carica avidamente tutte le immagini. Modello:

```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>
  );
}
```

Precarica l'immagine in evidenza (`onMouseEnter`) in modo che il clic sia istantaneo.

## Passo 6: Ricerca debounce

```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>
    </>
  );
}
```

## Passo 7: Sollevare regex ed evitare lo spread

Cattivo:

```tsx
function handleClick(text) {
  if (/[<>]/.test(text)) { ... }   // regex creata ad ogni clic
  setItems(prev => [...prev, ...newItems]); // spread nell'accumulatore
}
```

Buono:

```tsx
const TAG_REGEX = /[<>]/;  // sollevata allo scope del modulo

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

## Passo 8: Caricamento lazy sotto il fold

Raccomandazioni, recensioni, prodotti correlati: mettili sotto il fold e caricali in modo lazy con `dynamic` + `ssr: false` per widget puramente client, o caricali all'intersezione:

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

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

Per le sezioni renderizzate dal server, avvolgi in `<Suspense>` con PPR abilitato in modo che vengano trasmesse dopo il contenitore statico.

## Misurare il successo

Dopo aver implementato le correzioni, i miglioramenti dell'INP si mostrano in:

1. **Chrome DevTools** sotto Performance → Web Vitals overlay (sintetico, immediato).
2. **Il tuo RUM** con la libreria `web-vitals` (utente reale, quasi in tempo reale).
3. **Search Console → Core Web Vitals** (utente reale, p75 su 28 giorni, 2–4 settimane di ritardo).

Una tipica PDP che è partita da 350ms p75 INP e segue questa guida raggiunge 80–120ms p75 INP entro 2–4 settimane dal deployment.

## Come Ordiko spedisce PDP buone per INP per default

- Le parti statiche della PDP sono RSC (nessun JS client spedito).
- Il selettore di varianti utilizza `useOptimistic` + Server Actions.
- La galleria utilizza `startTransition` + precaricamento delle immagini al passaggio del mouse.
- La ricerca è debounced a 200ms con `AbortController`.
- Il carrello mostra l'ultimo conteggio noto in modo ottimistico.

INP p75 predefinito: 80–160ms.

## FAQ

**Perché l'INP è così molto più difficile dell'LCP?**
L'LCP è principalmente un problema di CDN e ottimizzazione delle immagini con correzioni ben comprese. L'INP è il tempo di esecuzione di JavaScript sull'interazione dell'utente; ogni clic può avere un collo di bottiglia diverso. Risolvere l'INP richiede disciplina a livello di componente su ogni elemento interattivo della pagina.

**I React Server Components risolvono automaticamente l'INP?**
In gran parte sì. RSC riduce il bundle JS spedito al browser, il che riduce il lavoro sul thread principale, migliorando l'INP. Ma le parti interattive della pagina hanno ancora bisogno di JS client — solo RSC non risolverà un componente client mal scritto che crea regex in onClick.

**Qual è il più grande killer di INP sulle PDP?**
Il pesante rendering lato client dei widget di raccomandazione ('Spesso acquistati insieme', 'Potresti anche apprezzare') che spediscono 50–200KB di JS e si idratano aggressivamente alla prima interazione. Sposta questi in Server Components o caricali in modo lazy dietro IntersectionObserver.

**Quanto può essere veloce una PDP ben ottimizzata su INP?**
Un INP p75 nel mondo reale sotto i 100ms è raggiungibile. Le PDP di Ordiko di default hanno 80–160ms p75 INP senza ottimizzazione manuale. Temi personalizzati che seguono i modelli in questa guida possono scendere sotto i 100ms in modo coerente.