Skip to content

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

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

Діаграма оптимізації 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 Vital?

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 та інженерія продуктивності або кейси з продакшену. Щоб обговорити ваші конкретні обмеження, запишіться на дзвінок.

Джерела

Схожі статті

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

Детальний розбір Next.js Partial Prerendering у продакшні. Механізм двофазної відповіді, правила розміщення Suspense-меж, взаємодія з Full Route Cache, дизайн fallback для нульового CLS, виміряні TTFB/LCP, порівняння з ISR+CSR та full SSR і фреймворк прийняття рішень.

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

Міграція на Next.js App Router з Pages Router: повний практичний гайд (2026)

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

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

React Server Components: як насправді працює архітектура з нульовим бандлом (2026)

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

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

Схожі послуги

Ці матеріали напряму пов’язані з послугами, які я реалізую в продакшені. Ознайомтесь із послугою або обговоріть свій випадок.