**TL;DR.** L'INP sur le PDP est amélioré en expédiant moins de JavaScript et en rendant les interactions optimistes. Utilisez les React Server Components pour les parties statiques. Utilisez `useOptimistic` pour l'échange de variantes et l'ajout au panier. Utilisez `startTransition` pour les mises à jour non urgentes. Débounchez la recherche avec `AbortController`. Élevez les regex et évitez le spread dans les gestionnaires d'événements. Objectif : < 200ms p75, atteignable < 100ms.

## Ce que mesure l'INP

L'Interaction to Next Paint (INP) mesure le temps le plus long entre une interaction et le rendu sur une page pendant la visite de l'utilisateur. Une "interaction" est un clic, un tap ou une pression de touche qui déclenche JavaScript.

L'INP est calculé comme le **pire cas** (excluant les rares valeurs aberrantes), pas la moyenne. Un clic de filtre lent peut ruiner votre score INP même si 99 autres interactions étaient rapides.

| Valeur INP      | Verdict           |
| -------------- | ----------------- |
| ≤ 200ms        | Bon               |
| 200ms – 500ms  | Besoin d'amélioration |
| > 500ms        | Mauvais           |

## Étape 1 : Profilage, ne devinez pas

Utilisez l'onglet Performance des Chrome DevTools pour enregistrer une session d'interaction PDP. Recherchez :

- Longs travaux (barres jaunes > 50ms) pendant les interactions.
- Reflows forcés (barres violettes).
- Attribution lourde pour les entrées INP.

Ou utilisez l'API Long Animation Frames (LoAF) dans votre 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 });
```

## Étape 2 : Différer le JS non critique

Les plus grands gains d'INP sur la plupart des PDP proviennent de la suppression ou du report des scripts que vous n'aviez pas besoin de charger de manière anticipée :

| Type de script             | Stratégie recommandée           |
| ----------------------- | ------------------------------ |
| Analytique (GA4, Plausible) | `afterInteractive`            |
| Pixels marketing         | `afterInteractive` ou `lazyOnload` |
| Widget de chat              | `lazyOnload` + IntersectionObserver |
| SDK de test A/B              | Blocage en ligne (rare) ou `beforeInteractive` |
| Moteur de recommandation    | Composant serveur ou IO paresseux    |
| Widget d'avis client   | Chargement paresseux avec IntersectionObserver |

Dans Next.js :

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

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

## Étape 3 : Convertir en composants serveur

La structure typique du PDP :

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

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

Seuls `AddToCartButton` et `VariantSelector` sont des composants client. Tout le reste reste sur le serveur. La taille du bundle diminue de 60 à 80 % par rapport à un PDP entièrement client.

## Étape 4 : Échange de variante optimiste

Le flux de clic de variante naïf :

1. L'utilisateur clique sur une variante.
2. POST à `/api/variant/select` avec le nouvel ID de variante.
3. Le serveur renvoie le prix/SKU/image mis à jour.
4. Re-rendu avec le nouvel état.

Total : 200–500ms de latence perçue. Score INP : mauvais.

Le flux optimiste :

```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 tombe à < 100ms car les mises à jour d'état optimistes se produisent de manière synchrone et le travail du serveur s'exécute dans une transition.

## Étape 5 : Galerie et miniature

La galerie de produits est un tueur d'INP courant si elle charge avidement toutes les images. Modèle :

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

Préchargez l'image survolée (`onMouseEnter`) afin que le clic soit instantané.

## Étape 6 : Recherche débouncée

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

## Étape 7 : Élever les regex et éviter le spread

Mauvais :

```tsx
function handleClick(text) {
  if (/[<>]/.test(text)) { ... }   // regex créée à chaque clic
  setItems(prev => [...prev, ...newItems]); // spread dans l'accumulateur
}
```

Bon :

```tsx
const TAG_REGEX = /[<>]/;  // élevé à la portée du module

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

## Étape 8 : Chargement paresseux en dessous de la ligne de flottaison

Recommandations, avis, produits connexes : placez-les en dessous de la ligne de flottaison et chargez-les paresseusement avec `dynamic` + `ssr: false` pour les widgets purement clients, ou chargez-les sur intersection :

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

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

Pour les sections rendues sur le serveur, enveloppez-les dans `<Suspense>` avec PPR activé afin qu'elles soient diffusées après la coque statique.

## Mesurer le succès

Après avoir déployé les corrections, les améliorations de l'INP se montrent dans :

1. **Chrome DevTools** sous Performance → Web Vitals overlay (synthétique, immédiat).
2. **Votre propre RUM** avec la bibliothèque `web-vitals` (utilisateur réel, quasi temps réel).
3. **Search Console → Core Web Vitals** (utilisateur réel, p75 sur 28 jours, 2–4 semaines de retard).

Un PDP typique qui a commencé à 350ms p75 INP et suit ce guide atteint 80–120ms p75 INP dans les 2 à 4 semaines suivant le déploiement.

## Comment Ordiko expédie des PDP avec un INP bon par défaut

- Les parties statiques du PDP sont RSC (aucun JS client expédié).
- Le sélecteur de variantes utilise `useOptimistic` + Server Actions.
- La galerie utilise `startTransition` + préchargement d'image au survol.
- La recherche est débouncée à 200ms avec `AbortController`.
- Le tiroir du panier rend le dernier compte connu de manière optimiste.

INP p75 par défaut : 80–160ms.

## FAQ

**Pourquoi l'INP est-il si difficile par rapport à l'LCP ?**
LCP est principalement un problème de CDN et d'optimisation d'image avec des corrections bien comprises. L'INP est le temps d'exécution JavaScript lors de l'interaction de l'utilisateur ; chaque clic peut avoir un goulot d'étranglement différent. Corriger l'INP nécessite une discipline au niveau des composants pour chaque élément interactif sur la page.

**Les React Server Components corrigent-ils automatiquement l'INP ?**
En grande partie oui. RSC réduit le bundle JS expédié au navigateur, ce qui réduit le travail sur le thread principal, ce qui améliore l'INP. Mais les parties interactives de la page ont toujours besoin de JS client — RSC à lui seul ne corrigera pas un composant client mal écrit qui crée des regex dans onClick.

**Quel est le plus grand tueur d'INP sur les PDP ?**
Le rendu lourd côté client des widgets de recommandation ('Fréquemment achetés ensemble', 'Vous pourriez aussi aimer') qui expédient 50 à 200 Ko de JS et hydratent agressivement lors de la première interaction. Déplacez-les vers des composants serveur ou chargez-les paresseusement derrière IntersectionObserver.

**Quelle vitesse un PDP bien optimisé peut-il atteindre sur l'INP ?**
Un INP p75 dans le monde réel sous 100ms est atteignable. Les PDP Ordiko par défaut à 80–160ms p75 INP sans optimisation manuelle. Les thèmes personnalisés qui suivent les modèles de ce guide peuvent obtenir régulièrement moins de 100ms.