**Кратко.** INP на PDP исправляется за счет уменьшения объема JavaScript и оптимистичных взаимодействий. Используйте React Server Components для статических частей. Используйте `useOptimistic` для смены варианта и добавления в корзину. Используйте `startTransition` для не срочных обновлений. Дебаунс поиска с помощью `AbortController`. Перенесите регулярные выражения и избегайте оператора spread в обработчиках событий. Цель: < 200ms p75, достижимо < 100ms.

## Что измеряет INP

Время взаимодействия до следующей отрисовки (INP) измеряет самое долгое время от взаимодействия до отрисовки на странице во время визита пользователя. "Взаимодействие" — это клик, нажатие или нажатие клавиши, которое запускает JavaScript.

INP рассчитывается как **худший случай** (исключая редкие выбросы), а не среднее значение. Один медленный клик фильтра может испортить ваш INP, даже если 99 других взаимодействий были быстрыми.

| Значение INP    | Вердикт           |
| ---------------- | ----------------- |
| ≤ 200ms          | Хорошо            |
| 200ms – 500ms    | Требует улучшения |
| > 500ms          | Плохо             |

## Шаг 1: Профилируйте, не догадывайтесь

Используйте вкладку Performance в Chrome DevTools, чтобы записать сессию взаимодействия с PDP. Обратите внимание на:

- Долгие задачи (желтые полосы > 50ms) во время взаимодействий.
- Принудительные переработки (пурпурные полосы).
- Большую `атрибуцию` для записей INP.

Или используйте API Long Animation Frames (LoAF) в вашем RUM:

```ts
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.duration > 200) {
      console.log('Долгая рамка:', entry);
    }
  }
});
observer.observe({ type: 'long-animation-frame', buffered: true });
```

## Шаг 2: Откладывайте некритичный JS

Наибольшие выигрыши INP на большинстве PDP приходят от удаления или откладывания скриптов, которые не нужно загружать немедленно:

| Тип скрипта             | Рекомендуемая стратегия           |
| ----------------------- | --------------------------------- |
| Аналитика (GA4, Plausible) | `afterInteractive`              |
| Маркетинговые пиксели    | `afterInteractive` или `lazyOnload` |
| Чат-виджет               | `lazyOnload` + IntersectionObserver |
| SDK A/B тестирования     | Встраиваемый блокирующий (редко) или `beforeInteractive` |
| Движок рекомендаций      | Серверный компонент или ленивый IO |
| Виджет отзывов клиентов   | Ленивый загрузка с IntersectionObserver |

В Next.js:

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

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

## Шаг 3: Переведите на серверные компоненты

Структура PDP обычно:

```tsx
// PDPPage (Серверный компонент)
import { ProductGallery } from './product-gallery';      // Сервер
import { ProductInfo } from './product-info';            // Сервер
import { AddToCartButton } from './add-to-cart-button';  // Клиент (интерактивный)
import { VariantSelector } from './variant-selector';    // Клиент (интерактивный)
import { RelatedProducts } from './related-products';    // Сервер
import { ReviewSummary } from './review-summary';        // Сервер

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

Только `AddToCartButton` и `VariantSelector` являются клиентскими компонентами. Все остальное остается на сервере. Размер пакета уменьшается на 60–80% по сравнению с полностью клиентским PDP.

## Шаг 4: Оптимистичная смена варианта

Наивный поток клика по варианту:

1. Пользователь кликает на вариант.
2. POST на `/api/variant/select` с новым ID варианта.
3. Сервер возвращает обновленную цену/SKU/изображение.
4. Повторная отрисовка с новым состоянием.

Итого: 200–500ms воспринимаемой задержки. Оценка INP: плохая.

Оптимистичный поток:

```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 снижается до < 100ms, потому что оптимистичное состояние обновляется синхронно, а работа сервера выполняется в переходе.

## Шаг 5: Галерея и миниатюра

Галерея продукта — это распространенный убийца INP, если она немедленно загружает все изображения. Шаблон:

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

Предварительно загружайте наведенное изображение (`onMouseEnter`), чтобы клик был мгновенным.

## Шаг 6: Дебаунс поиска

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

## Шаг 7: Перенос регулярных выражений и избегание spread

Плохо:

```tsx
function handleClick(text) {
  if (/[<>]/.test(text)) { ... }   // регулярное выражение создается при каждом клике
  setItems(prev => [...prev, ...newItems]); // spread в аккумуляторе
}
```

Хорошо:

```tsx
const TAG_REGEX = /[<>]/;  // перенесено в область модуля

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

## Шаг 8: Ленивый загрузка ниже сгиба

Рекомендации, отзывы, связанные продукты: разместите их ниже сгиба и загружайте лениво с помощью `dynamic` + `ssr: false` для чисто клиентских виджетов или загружайте при пересечении:

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

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

Для серверных секций оберните в `<Suspense>` с включенным PPR, чтобы они стримились после статической оболочки.

## Измерение успеха

После развертывания исправлений улучшения INP отображаются в:

1. **Chrome DevTools** в разделе Performance → Web Vitals overlay (синтетические, немедленные).
2. **Вашем собственном RUM** с библиотекой `web-vitals` (реальные пользователи, почти в реальном времени).
3. **Search Console → Core Web Vitals** (реальные пользователи, p75 за 28 дней, задержка 2–4 недели).

Типичный PDP, который начинался с 350ms p75 INP и следует этому руководству, достигает 80–120ms p75 INP в течение 2–4 недель после развертывания.

## Как Ordiko по умолчанию поставляет PDP с хорошим INP

- Статические части PDP являются RSC (без клиентского JS).
- Выбор варианта использует `useOptimistic` + серверные действия.
- Галерея использует `startTransition` + предварительную загрузку изображений при наведении.
- Поиск дебаунсится на 200ms с помощью `AbortController`.
- Корзина отображает последний известный счет оптимистично.

Стандартный p75 INP: 80–160ms.

## Часто задаваемые вопросы

**Почему INP так намного сложнее, чем LCP?**
LCP в основном является проблемой CDN и оптимизации изображений с хорошо понятными решениями. INP — это время выполнения JavaScript при взаимодействии пользователя; каждый клик может иметь разные узкие места. Исправление INP требует дисциплины на уровне компонентов для каждого интерактивного элемента на странице.

**Исправляет ли React Server Components INP автоматически?**
В значительной степени да. RSC уменьшает объем JS, отправляемого в браузер, что снижает нагрузку на главный поток, что улучшает INP. Но интерактивные части страницы все еще нуждаются в клиентском JS — RSC сама по себе не исправит плохо написанный клиентский компонент, который создает регулярные выражения в onClick.

**Какой самый большой убийца INP на PDP?**
Сильная рендеринг клиентских виджетов рекомендаций ('Часто покупают вместе', 'Вам также может понравиться'), которые отправляют 50–200KB JS и агрессивно гидратируются при первом взаимодействии. Переместите их в серверные компоненты или лениво загружайте за IntersectionObserver.

**Как быстро может оптимизированный PDP достичь INP?**
В реальном мире p75 INP ниже 100ms достижимо. PDP Ordiko по умолчанию имеют 80–160ms p75 INP без ручной оптимизации. Пользовательские темы, которые следуют шаблонам в этом руководстве, могут стабильно достигать ниже 100ms.