Skip to content

INP-оптимизация в Next.js 16: почему 43% сайтов проваливают метрику и как это исправить

INP заменила FID в качестве Core Web Vital в марте 2024 года. Два года спустя 43% мобильных сайтов по-прежнему не проходят порог в 200 мс. Это не проблема инструментов - это архитектурная проблема.

Диаграмма оптимизации INP - планировщик задач главного потока
Автор:Опубликовано:Обновлено:Время чтения:14 мин

Senior Frontend Architect, 10+ лет опыта построения production-проектов на Next.js. Contentful Certified Professional (2024). Специализация: React Server Components, headless eCommerce, инженерия Core Web Vitals.

12 марта 2024 года Google официально вывел First Input Delay (FID) из Core Web Vitals в пользу Interaction to Next Paint (INP). Переход обнажил огромное слепое пятно: 94% сайтов имели «хороший» FID, но на старте только 54% проходили более строгий порог INP. По данным HTTP Archive 2025 Web Almanac, 43% мобильных origin до сих пор не укладываются в 200 мс - и за два года это число почти не сдвинулось. В этом посте я разбираю первопричины, раскладываю три фазы измерения и показываю шесть подходов к оптимизации, специфичных для Next.js 16, с опорой на продакшен-бенчмарки headless-магазинов.

Почему Google заменил FID на INP в Core Web Vitals?

FID был принципиально ограниченной прокси-метрикой. Он измерял исключительно задержку ввода перед самым первым подходящим взаимодействием на странице - обычно кликом или тапом до того, как JavaScript полностью гидратировал документ. Статистический изъян здесь фундаментальный: пользователь, который нажимает «В корзину» на третьей секунде сессии, уже после завершения гидратации, не вносит в FID вообще никакого сигнала - какой бы вялой ни была реакция интерфейса. Страница могла получить «хороший» FID и при этом отвечать на фильтры каталога с задержкой 800 мс - ровно там, где намерение купить максимально. INP исправляет этот дефект: он наблюдает задержку всех подходящих взаимодействий за всю сессию и отдаёт одно репрезентативное значение примерно на 98-м перцентиле распределения. Сдвиг тут принципиальный. FID мерил, как страница стартует. INP мерит, как она себя ведёт, когда с ней уже работают.

  • Исторический контекст: FID появился в 2018 году как временная прокси-метрика, потому что Time to Interactive (TTI) оказался слишком нестабильным для полевых измерений. Его замену авторы спецификации планировали с самого начала.
  • Падение на 5 пунктов: HTTP Archive 2025 Web Almanac фиксирует снижение общей доли прохождения Core Web Vitals на мобильных на 5 процентных пунктов после перехода с FID на INP - это десятки миллионов URL, переклассифицированных из «Good» в «Needs Improvement».
  • Сигнал ранжирования: В мае 2023 года Google подтвердил (developers.google.com/search/blog/2023/05/introducing-inp), что INP входит в сигналы page experience, используемые в органическом ранжировании. Это делает метрику вопросом бизнеса, а не только UX.

Как браузер на самом деле измеряет INP?

Спецификация W3C Event Timing API (WICG) определяет INP через точный алгоритм из трёх подынтервалов. Для каждого подходящего PointerEvent, KeyboardEvent или InputEvent браузер записывает: (1) задержку ввода - время от метки аппаратного прерывания до момента, когда начинает выполняться первый обработчик события; (2) время обработки - суммарную длительность всех синхронных обработчиков, включая синтетические события React и вызванные ими мутации DOM; (3) задержку отрисовки - время от конца последнего обработчика до момента, когда браузер отдаёт получившийся кадр композитору. Итоговый INP - это взаимодействие на 98-м перцентиле среди всех записанных за сессию (минимум одно взаимодействие, мягкий потолок - 50 для длинных сессий, согласно спецификации).

  • Good: INP ≤ 200 миллисекунд. Пользователь воспринимает интерфейс как мгновенно отзывчивый. Исследования времени реакции человека кладут порог прямого восприятия причинности между действием и эффектом в диапазон 100–200 мс.
  • Needs Improvement: 201–500 миллисекунд. Заметное отставание вносит когнитивный диссонанс - пользователь осознаёт, что система думает, и это выбивает его из потока взаимодействия.
  • Poor: INP > 500 миллисекунд. Поведение интерфейса неотличимо от зависшего. Исследования UX в Google связывают этот диапазон с измеримым ростом доли отказов.
  • Правило 75-го перцентиля: Чтобы URL получил в полевых данных CrUX статус «Good», не менее 75% всех сессий по этому URL должны показать «хороший» INP.

Отзывчивость - вопрос доступности, а не только приятного UX. Пользователи вспомогательных технологий, навигации только с клавиатуры, а также люди с моторными и когнитивными особенностями страдают от подвисших взаимодействий непропорционально сильно: задержку в 500 мс быстрый пользователь просто стряхнёт, а у пользователя скринридера она ломает модель того, что сейчас происходит на странице. Оптимизация INP - это работа для всех; сочетайте её с паттернами инклюзивного взаимодействия из гайда по веб-доступности (a11y).

Каковы четыре главных причины плохого INP?

Почти всё сводится к четырём причинам: длинные задачи монополизируют главный поток, дорогая гидратация, тряска фазы рендера и конкуренция сторонних скриптов. Порядок - по частоте, и одна только первая покрывает примерно две трети нарушений. Данные собраны с 40+ продакшен-фронтендов интернет-магазинов, преимущественно витрин Magento 2 и Shopify Plus, переехавших на headless Next.js. Причины чаще сочетаются, чем встречаются поодиночке.

  • Монополизация главного потока длинными задачами (~67% нарушений): Один синхронный блок JavaScript занимает главный поток дольше 50 мс, из-за чего браузер не может обработать ни одно событие ввода из очереди. Порог в 50 мс взят из модели RAIL (Response-Animation-Idle-Load), которая задаёт его как максимальную длительность задачи, совместимую с бесшовным взаимодействием на 60 кадрах в секунду.
  • Избыточная стоимость гидратации (~20% нарушений): На Moto G Power - стандартном тестовом устройстве, представляющем медианное Android-железо в мире - гидратация React 18 для страницы каталога из 200 компонентов может занять 350–800 мс непрерывного главного потока. Это устойчивое окно полной неотзывчивости сразу после загрузки, ровно тогда, когда пользователь пробует первое взаимодействие.
  • Тряска фазы рендера (~10% нарушений): Плохо выстроенное дерево компонентов вызывает каскад перерендеров на каждое обновление состояния. Без границ мемоизации один вызов setFilter() на странице каталога приводит к O(n) вычислениям компонентов, где n - количество отрисованных карточек товара.
  • Конкуренция сторонних скриптов (~3% нарушений, но непропорциональный эффект на затронутых страницах): Маркетинговые пиксели, виджеты чата и библиотеки поведенческой аналитики борются за главный поток. По данным HTTP Archive 2025 Web Almanac, медианная страница интернет-магазина грузит 47 сторонних запросов и получает от них медианные 1,2 секунды блокировки главного потока.

Какой подынтервал INP - ваша проблема: задержка ввода, обработка или отрисовка?

Сначала выясните, в каком из трёх подынтервалов сидит проблема, и только потом чините. Перепутать задержку ввода с временем обработки легко, а обходится это дорого: первое лечится планированием задач, второе - рендерингом, и работа не по адресу не сдвинет число вообще. Панель Performance в Chrome DevTools показывает все три подынтервала в дорожке «Interactions» при записи трассы.

  • Задержка ввода → проблема планирования: В DevTools видна как промежуток между меткой события и запуском первого обработчика. Причина - длинные задачи, вытесняющие очередь событий. Лечение: декомпозиция задач через scheduler.yield().
  • Время обработки → проблема рендеринга: Плотный синхронный блок JavaScript во flame chart сразу после вызова события. Доминирует согласование React. Лечение: React.memo, useMemo, startTransition, компилятор React 19.
  • Задержка отрисовки → проблема лейаута: Промежуток между завершением обработчика и коммитом кадра, вызванный принудительным синхронным лейаутом (чтение element.offsetHeight после записи в DOM). Лечение: группируйте чтения и записи DOM; применяйте contain: layout style paint на компонентах, которые перерисовываются независимо.

Как scheduler.yield() снижает INP при длинных задачах?

scheduler.yield() возвращает Promise, который резолвится при первой возможности после того, как браузер обработал более приоритетную работу из очереди, включая события пользовательского ввода. Полная поддержка во всех браузерах (Chrome 115+, Firefox 124+, Safari 17.4+) сложилась к первому кварталу 2025 года. Это самый точный механизм разбиения длинных процедур инициализации, которые иначе монополизируют главный поток.

  • Антипаттерн: useEffect(() => { initAnalytics(); hydrateCart(); loadPersonalization(); registerServiceWorker(); }, []) - составная задача на 300–700 мс на среднем Android, которая блокирует любой ввод на всю свою длительность.
  • Паттерн с уступкой: async function init() { await initAnalytics(); await scheduler.yield(); await hydrateCart(); await scheduler.yield(); await loadPersonalization(); } - каждый await scheduler.yield() создаёт явную границу задачи, позволяя обработать накопившиеся click и keydown между шагами.
  • Кроссбраузерный полифилл: const yieldToMain = () => 'scheduler' in window ? scheduler.yield() : new Promise(resolve => setTimeout(resolve, 0)); - setTimeout(fn, 0) даёт ту же семантику границы задачи, но без наследования приоритета, которое есть у нативного API.
  • Измерение: После декомпозиции одна задача на 500 мс должна распасться в таймлайне DevTools на несколько задач короче 50 мс. Снижение Total Blocking Time (TBT) - лабораторный прокси ожидаемого улучшения INP.

Действительно ли компилятор React 19 улучшает INP, и насколько?

React Compiler (стабильная версия v1.0, октябрь 2025) выполняет проход статического анализа и автоматически вставляет мемоизацию на каждой границе компонента и хука, где ссылочную стабильность можно доказать статически. Это снимает необходимость в ручных React.memo, useMemo и useCallback - аннотациях, которых в продакшен-кодовых базах хронически не хватает из-за невнимательности и когнитивной нагрузки ручного управления мемоизацией. Ранние продакшен-бенчмарки Meta и Vercel показывают снижение числа перерендеров на 20–40% для интерфейсов с интенсивным CRUD и соответствующее улучшение INP на 30–80 мс на бенчмарках фильтрации каталога.

  • Включение в Next.js 15.1+: Добавьте experimental: { reactCompiler: true } в next.config.ts. Интегрируется с существующим пайплайном Babel/SWC, для подходящих компонентов правки кода не нужны.
  • Анализ пригодности: npx react-compiler-healthcheck показывает долю компонентов, пригодных к оптимизации. Типичная унаследованная кодовая база набирает 60–80%.
  • Архитектурное ограничение: Компилятор отказывается от компонентов с мутируемыми локальными переменными, нестандартными реализациями хуков или императивными операциями с DOM - для полного покрытия их придётся отрефакторить.
  • Для проектов на React 18: Тот же эффект приближается вручную: React.memo на каждый элемент списка, useMemo на производные данные, уходящие в пропсы, и useCallback на обработчики, передаваемые мемоизированным потомкам.

Помогают ли React Server Components с INP, и сколько JS они убирают?

Механика простая. Компоненты без browser API, обработчиков событий и клиентского состояния не добавляют в клиентский бандл ни байта JavaScript. Дальше по цепочке: меньше бандл → быстрее парсинг и компиляция → короче гидратация → меньше конкуренции за главный поток → ниже задержка ввода. Опубликованный анализ Vercel показывает, что перенос поддерева клиентских компонентов на 200 КБ в RSC снижает Time to Interactive на 800 мс – 1,2 с на медианном Android-устройстве, напрямую сжимая самое опасное окно уязвимости по INP.

  • Граница 'use client' как контракт бюджета производительности: В App Router все компоненты по умолчанию серверные. 'use client' создаёт границу клиентского бандла - всё поддерево ниже компилируется в клиент. Поставленная слишком высоко директива отправляет в браузер в 5–10 раз больше JavaScript, чем требует само интерактивное поведение.
  • Паттерн извлечения клиентских островов: Карточке товара из 12 компонентов 'use client' нужен, как правило, только на кнопке «В корзину». Остальные 11 остаются серверными и не добавляют в клиентский бандл ничего.
  • Отложенная гидратация: const Widget = React.lazy(() => import('./Widget')) внутри <Suspense fallback={<Skeleton />}> откладывает гидратацию компонентов ниже первого экрана, концентрируя клиентский бюджет на взаимодействиях выше сгиба, критичных для INP.
  • Прокси для измерения: Сравните Total Blocking Time в Lighthouse до и после перехода на RSC. TBT суммирует все миллисекунды сверх 50 мс по длинным задачам главного потока - самый сильный доступный лабораторный прокси для INP.

Когда стоит оборачивать обновления состояния в startTransition ради INP?

startTransition говорит React, что обновление несрочное. Тогда конкурентный рендерер вправе прервать эту работу и уступить, если пользователь сделал что-то более важное. Это правильный инструмент для дорогих переходов интерфейса - фильтрации каталога, переключения представлений дашборда, отрисовки насыщенных данными таблиц, - когда пользователь выразил навигационное намерение, но результат не обязан появиться в пределах одного кадра анимации.

  • Канонический паттерн фильтра: const [isPending, startTransition] = useTransition(); function onFilterChange(v) { startTransition(() => { setFilter(v); }); } - если пользователь выберет следующий фильтр до завершения первого рендера, React отбросит прерванную работу и начнёт заново с последним значением, сохранив отзывчивость ввода на всём протяжении.
  • Важное различие: Оборачивать в startTransition все обновления состояния - антипаттерн. Срочные обновления (состояние нажатой кнопки, переключение чекбокса, значение поля ввода) обязаны остаться синхронными. Отложенная отрисовка собственных нажатий пользователя ощущается хуже, чем умеренно плохой INP.
  • isPending для скелетонов: Условно отрисовывайте скелетон во время отложенных рендеров - это предотвращает сдвиги лейаута (нарушения CLS), пока идёт конкурентный рендер.
  • Интеграция с Suspense: Если отложенное обновление состояния запускает загрузку данных, которая бросает Promise, React покажет ближайший фолбэк Suspense вместо блокировки, соединяя оптимистичную интерактивность с асинхронной загрузкой.

Как изолировать сторонние скрипты, чтобы они не блокировали INP?

Сторонние скрипты вы не контролируете, и своим кодом эту проблему не закрыть. Они борются за тот же главный поток, а обновляются без вашего ведома. HTTP Archive 2025 Web Almanac фиксирует, что медианная страница интернет-магазина грузит 47 сторонних запросов и получает от стороннего JavaScript медианные 1,2 секунды блокировки главного потока. На хорошо оптимизированных витринах сторонние скрипты часто оказываются крупнейшим оставшимся вкладом в INP.

  • Partytown для выноса в Web Worker: Переносит выполнение сторонних скриптов из главного потока в выделенный Web Worker через синхронно-асинхронный мост поверх SharedArrayBuffer. Измеренный эффект в продакшене: снижение Total Blocking Time на 200–600 мс на страницах с 5+ маркетинговыми скриптами. Интеграция: <Partytown> в app/layout.tsx, подходящие скрипты помечаются type='text/partytown'.
  • Паттерн фасада: Замените встроенные виджеты чата (Intercom, Zendesk, Drift) и видеоплееры лёгкими статичными фасадами-картинками. Настоящий скрипт грузится только на mouseenter или первом touchstart по фасаду. Типичный виджет чата добавляет 80–200 КБ JavaScript и 40–120 мс парсинга - фасад убирает это с критического пути.
  • Стратегии загрузки Next.js Script: strategy='lazyOnload' для некритичных пикселей и карт кликов (откладывает до полного простоя браузера). strategy='afterInteractive' для фреймворков A/B-тестов (выполняется после того, как страница стала интерактивной). strategy='beforeInteractive' оставьте исключительно для полифиллов и платформ управления согласиями.

Какие паттерны Next.js 16 конкретно помогают INP в продакшене?

У Next.js 16 есть несколько вещей, которые двигают INP заметно. Работают они на уровне рендеринга, а не отдельного компонента, поэтому и эффект другого масштаба.

  • Partial Prerendering (PPR): Маршрут мгновенно отдаёт из edge-кэша статическую оболочку HTML - интерфейс выше сгиба, навигацию, критичный лейаут, - пока динамический персонализированный контент асинхронно стримится через Suspense. Статическая оболочка интерактивна ещё до прихода динамических данных, что развязывает отзывчивость интерфейса и задержку загрузки данных. Включается через export const experimental_ppr = true на уровне сегмента маршрута.
  • next/dynamic с ssr: false: Для компонентов на Three.js, библиотек графиков (Recharts, D3), редакторов текста (Tiptap, Lexical) и рендереров карт: const Heavy = dynamic(() => import('./Heavy'), { ssr: false, loading: () => <Skeleton /> }). Исключает компонент из серверного рендера и откладывает парсинг JavaScript до окончания первичной гидратации.
  • Кеширование route handlers: Высокочастотные маршруты данных (каталоги, меню навигации, счётчики фасетов) должны отдавать Cache-Control: s-maxage=60, stale-while-revalidate=300. Некешированные водопады API систематически добавляют время обработки к каждой клиентской навигации, поднимая INP навигационных взаимодействий.
  • useOptimistic для мгновенной обратной связи: Отрисовывает оптимистичное состояние интерфейса сразу по взаимодействию, ещё до ответа сервера. Для добавления в корзину, переключения избранного и отправки форм воспринимаемый INP стремится к нулю. При ошибке серверного действия оптимистичное состояние откатывается автоматически.

Как выстроить измерение и наблюдаемость INP?

Без полевых замеров оптимизировать INP бессмысленно: вы просто не узнаете, стало лучше или нет. Разницу между лабораторией (Lighthouse) и полем (CrUX) нужно держать в голове постоянно. Лаборатория гоняет идеальное железо, а INP ломается на средних Android в нестабильной сети. Chrome User Experience Report (CrUX) агрегирует метрики реальных пользователей на 75-м перцентиле фактического трафика - это авторитетный источник истины для классификации Core Web Vitals.

  • CrUX API для полевых данных: Запрос к https://chromeuxreport.googleapis.com/v1/records:queryRecord с вашим origin или URL. Гистограмма interaction_to_next_paint даёт фактическое значение INP на 75-м перцентиле по реальным пользователям.
  • Event Timing API для собственного RUM: const observer = new PerformanceObserver((list) => { for (const entry of list.getEntries()) { if (entry.interactionId > 0) { analytics.track('inp_candidate', { duration: entry.duration, type: entry.name, element: entry.target?.tagName }); } } }); observer.observe({ type: 'event', buffered: true, durationThreshold: 16 }); - собирает все подходящие взаимодействия с атрибуцией до компонента.
  • web-vitals.js как канонический замер: Официальная библиотека Google (npm install web-vitals) реализует ровно тот алгоритм INP, который использует CrUX. onINP(({ value, rating }) => sendToAnalytics({ inp: value, rating })); - гарантирует совпадение вашего замера с классификацией CrUX.
  • Регрессионный гейт в Lighthouse CI: assert: { assertions: { 'total-blocking-time': ['error', { maxNumericValue: 300 }] } } в CI/CD. TBT - самый сильный доступный лабораторный прокси для INP и надёжный гейт против регрессий перед деплоем.

Какого улучшения INP стоит ожидать после применения этих исправлений?

Рассчитывайте на 30–280 мс с INP на каждый применённый вектор и на переход из «Needs Improvement» в «Good», если базовый уровень не катастрофический с самого начала. Цифры ниже - медианные результаты по нескольким продакшен-витринам headless-коммерции, и относиться к ним следует как к инженерным оценкам: базовая производительность, распределение устройств и объём стороннего кода различаются значительно.

  • Переход на RSC на страницах каталога: Снижение INP на 120–280 мс на медианных мобильных устройствах. Основной механизм - исчезновение работы по гидратации для тех 80–90% отрисованных компонентов, которые не интерактивны.
  • Декомпозиция через scheduler.yield(): Снижение задержки ввода на 60–150 мс на затронутой странице. Основной механизм - дробление задач создаёт окна для обработки накопившихся событий ввода.
  • Компилятор React 19: Снижение числа перерендеров на 20–40% на бенчмарках фильтрации. Снижение INP на 30–80 мс на страницах каталога с фасетной навигацией.
  • Изоляция сторонних скриптов (Partytown / lazyOnload): Снижение TBT на 200–600 мс на страницах с 5+ маркетинговыми скриптами.
  • Partial Prerendering: Убирает задержку загрузки данных, блокирующую гидратацию персонализированного контента - часто это 300–800 мс занятости главного потока, снятых с критического пути.
  • Суммарно (все векторы): Переход из «Needs Improvement» в «Good» стабильно достижим там, где базовый INP ниже 450 мс. Сайтам выше 450 мс к точечным оптимизациям потребуются архитектурные изменения. У сайтов с «хорошими» Core Web Vitals задокументирована на 24% более высокая мобильная конверсия, чем у сайтов с «плохими» (исследование Google по eCommerce, 2024).

Заключение

INP - не мелкая правка метрики, а сигнал о том, насколько хорошо ваш фронтенд делит главный поток браузера. Доля отказов в 43% спустя два года говорит, что маленькими заплатками это не лечится. Чтобы увести INP под 200 мс, нужно понимать планировщик задач браузера, пайплайн рендеринга React, разницу между срочными и несрочными обновлениями состояния и то, как свой и сторонний код конкурируют за один и тот же поток. Шесть векторов из этого поста дают практическую отправную точку. Если ваш INP выше 200 мс, у вас конкретная инженерная проблема с конкретным решением - начните с той первопричины, на которую указывает трасса в DevTools.

Что читать дальше и услуги: Если Search Console уже показывает проваленную оценку, начните с разбора того, как читать этот отчёт, и прогоните бесплатный аудит сайта, чтобы увидеть свою полевую INP рядом с остальными сигналами. Для структурированного архитектурного разбора INP-профиля вашей витрины есть услуга Core Web Vitals и инженерия производительности или кейсы из продакшена. Чтобы обсудить ваши конкретные ограничения, запишитесь на созвон.

Источники

Похожие статьи

React Server Components: как на самом деле работает архитектура с нулевым бандлом (2026)

Глубокое погружение в React Server Components - модель рендеринга, которая убирает клиентский JavaScript для серверных поддеревьев. Wire-формат RSC, семантика границ, async-загрузка данных, Suspense-стриминг, Partial Prerendering и React 19 Compiler - с реальными бенчмарками и практическим фреймворком принятия решений.

ReactNext.jsArchitecture
Читать статью

Миграция на Next.js App Router с Pages Router: полный практический гайд (2026)

Практический гайд по миграции продакшн-приложений Next.js с Pages Router на App Router. Стоимостная модель гидратации RSC, механика модульного графа webpack и директивы 'use client', внутреннее устройство Data Cache, инкрементальная стратегия, разбор типичных ошибок и фреймворк принятия решений.

Next.jsArchitectureMigration
Читать статью

Partial Prerendering (PPR) в продакшне: архитектурные паттерны (2026)

Детальный разбор Next.js Partial Prerendering в продакшне. Механизм двухфазного ответа (статический shell с CDN + стриминговые dynamic holes), правила размещения Suspense-границ, взаимодействие с Full Route Cache, дизайн fallback для нулевого CLS, измеренные TTFB/LCP, сравнение с ISR+CSR и full SSR, известные ограничения и фреймворк принятия решений.

Next.jsPerformancePPR
Читать статью

Похожие услуги

Эти материалы напрямую связаны с услугами, которые я реализую в продакшене. Изучите услугу или обсудите свой случай.