**TL;DR.** INP no PDP é corrigido ao enviar menos JavaScript e tornar as interações otimistas. Use React Server Components para partes estáticas. Use `useOptimistic` para troca de variantes e adição ao carrinho. Use `startTransition` para atualizações não urgentes. Debounce a busca com `AbortController`. Eleve regex e evite spread em manipuladores de eventos. Meta: < 200ms p75, alcançável < 100ms.

## O que o INP mede

Interaction to Next Paint (INP) mede o maior tempo de interação até a pintura em uma página durante a visita do usuário. Uma "interação" é um clique, toque ou pressionamento de tecla que aciona JavaScript.

O INP é calculado como o **pior caso** (excluindo outliers raros), não a média. Um clique lento em um filtro pode arruinar sua pontuação de INP, mesmo que 99 outras interações tenham sido rápidas.

| Valor do INP    | Veredicto         |
| ---------------- | ----------------- |
| ≤ 200ms          | Bom               |
| 200ms – 500ms    | Precisa de melhorias |
| > 500ms          | Ruim              |

## Passo 1: Perfil, não adivinhe

Use a aba Performance do Chrome DevTools para gravar uma sessão de interação do PDP. Procure por:

- Tarefas longas (barras amarelas > 50ms) durante interações.
- Reflows forçados (barras roxas).
- Atribuição pesada para entradas de INP.

Ou use a API Long Animation Frames (LoAF) em seu 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: Adiar JS não crítico

As maiores vitórias de INP na maioria dos PDPs vêm da remoção ou adiamento de scripts que você não precisava carregar de forma ansiosa:

| Tipo de script             | Estratégia recomendada           |
| -------------------------- | -------------------------------- |
| Analytics (GA4, Plausible) | `afterInteractive`               |
| Pixels de marketing         | `afterInteractive` ou `lazyOnload` |
| Widget de chat              | `lazyOnload` + IntersectionObserver |
| SDK de teste A/B           | Inline blocking (raro) ou `beforeInteractive` |
| Motor de recomendação      | Server Component ou lazy IO      |
| Widget de avaliações de clientes | IntersectionObserver lazy load |

Em Next.js:

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

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

## Passo 3: Converter para Server Components

A estrutura do PDP tipicamente:

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

Apenas `AddToCartButton` e `VariantSelector` são Client Components. Todo o resto permanece no servidor. O tamanho do pacote cai 60–80% em comparação com um PDP totalmente cliente.

## Passo 4: Troca otimista de variantes

O fluxo de clique de variante ingênuo:

1. O usuário clica na variante.
2. POST para `/api/variant/select` com o novo ID da variante.
3. O servidor retorna o preço/SKU/imagem atualizado.
4. Re-renderiza com o novo estado.

Total: 200–500ms de latência percebida. Pontuação de INP: ruim.

O fluxo otimista:

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

O INP cai para < 100ms porque o estado otimista é atualizado de forma síncrona e o trabalho do servidor é executado em uma transição.

## Passo 5: Galeria e miniatura

A galeria de produtos é um assassino comum de INP se carregar ansiosamente todas as imagens. Padrão:

```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é-carregue a imagem sobre a qual o mouse passa (`onMouseEnter`) para que o clique seja instantâneo.

## Passo 6: Busca debounced

```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: Elevar regex e evitar spread

Ruim:

```tsx
function handleClick(text) {
  if (/[<>]/.test(text)) { ... }   // regex criado em cada clique
  setItems(prev => [...prev, ...newItems]); // spread no acumulador
}
```

Bom:

```tsx
const TAG_REGEX = /[<>]/;  // elevado ao escopo do módulo

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

## Passo 8: Carregar preguiçosamente abaixo da dobra

Recomendações, avaliações, produtos relacionados: coloque-os abaixo da dobra e carregue-os preguiçosamente com `dynamic` + `ssr: false` para widgets puramente clientes, ou carregue na interseção:

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

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

Para seções renderizadas no servidor, envolva em `<Suspense>` com PPR habilitado para que sejam transmitidas após a shell estática.

## Medindo o sucesso

Após implantar correções, as melhorias de INP aparecem em:

1. **Chrome DevTools** sob Performance → Web Vitals overlay (sintético, imediato).
2. **Seu próprio RUM** com a biblioteca `web-vitals` (usuário real, quase em tempo real).
3. **Search Console → Core Web Vitals** (usuário real, p75 ao longo de 28 dias, 2–4 semanas de atraso).

Um PDP típico que começou com 350ms p75 INP e segue este guia alcança 80–120ms p75 INP dentro de 2–4 semanas após a implantação.

## Como a Ordiko envia PDPs bons em INP por padrão

- As partes estáticas do PDP são RSC (sem JS do cliente enviado).
- O seletor de variantes usa `useOptimistic` + Server Actions.
- A galeria usa `startTransition` + pré-carregamento de imagem ao passar o mouse.
- A busca é debounced em 200ms com `AbortController`.
- O carrinho renderiza a contagem conhecida mais recente de forma otimista.

INP p75 padrão: 80–160ms.

## FAQ

**Por que o INP é muito mais difícil que o LCP?**
O LCP é principalmente um problema de CDN e otimização de imagem com correções bem compreendidas. O INP é o tempo de execução do JavaScript na interação do usuário; cada clique pode ter um gargalo diferente. Corrigir o INP requer disciplina em nível de componente em todos os elementos interativos na página.

**Os React Server Components corrigem o INP automaticamente?**
Em grande parte, sim. O RSC reduz o pacote de JS enviado para o navegador, o que reduz o trabalho na thread principal, melhorando o INP. Mas as partes interativas da página ainda precisam de JS do cliente — o RSC sozinho não corrigirá um componente cliente mal escrito que cria regex em onClick.

**Qual é o maior assassino de INP em PDPs?**
A renderização pesada do lado do cliente de widgets de recomendação ('Frequentemente comprados juntos', 'Você também pode gostar') que enviam 50–200KB de JS e hidratam agressivamente na primeira interação. Mova esses para Server Components ou carregue-os preguiçosamente atrás do IntersectionObserver.

**Quão rápido um PDP bem otimizado pode ficar em INP?**
INP p75 no mundo real abaixo de 100ms é alcançável. Os PDPs da Ordiko têm como padrão 80–160ms p75 INP sem otimização manual. Temas personalizados que seguem os padrões deste guia podem ficar abaixo de 100ms de forma consistente.