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 та інженерія продуктивності або кейси з продакшену. Щоб обговорити ваші конкретні обмеження, запишіться на дзвінок.
Джерела
- W3C WICG - специфікація Event Timing API
- Google web.dev - Interaction to Next Paint (INP)
- Google web.dev - як оптимізувати INP
- Google web.dev - оптимізація довгих задач
- HTTP Archive - 2025 Web Almanac, розділ про продуктивність
- Vercel - покращення INP з React 18 і Suspense
- Google Search Central - введення INP у Core Web Vitals
- React - React Compiler
- DebugBear - знайомство зі scheduler.yield
- Addy Osmani - історія Core Web Vitals
