Почему INP — самая проблемная метрика Core Web Vitals в 2026 году
Interaction to Next Paint (INP) пришла на смену First Input Delay (FID) в марте 2024-го — и, честно говоря, жизнь веб-разработчиков с тех пор стала заметно сложнее. Если FID измеряла задержку только первого взаимодействия, то INP оценивает каждое: клики, тапы, нажатия клавиш — всё на протяжении жизненного цикла страницы. А итоговый балл определяет самая медленная операция (точнее — значение на 75-м перцентиле).
Цифры, мягко говоря, не радуют: 43% сайтов не проходят порог в 200 мс, который Google считает «хорошим» INP. Для сравнения — FID успешно проходили более 93% сайтов. То есть почти половина веба прямо сейчас демонстрирует плохую отзывчивость. И это не абстрактная статистика — речь о реальном влиянии на позиции в поиске, показатель отказов и конверсии.
В этом руководстве мы разберём INP по кусочкам, научимся находить проблемы через Chrome DevTools и Long Animation Frames API, а потом пошагово оптимизируем каждую фазу — от scheduler.yield() до Web Workers. Поехали.
Три фазы INP: из чего складывается задержка
Любое взаимодействие пользователя с вашим сайтом проходит через три последовательные фазы. Суммарная длительность всех трёх и даёт финальное значение INP.
1. Input Delay — задержка до начала обработки
Это время от момента клика (или нажатия клавиши) до момента, когда браузер реально начинает выполнять обработчик события. Звучит просто, но вот в чём подвох: если главный поток в этот момент занят чем-то другим — парсит тяжёлый скрипт, обрабатывает данные стороннего виджета — ваше взаимодействие будет терпеливо ждать в очереди.
2. Processing Time — время обработки
Здесь выполняются все обработчики событий (event listeners), привязанные к взаимодействию. И это не только ваш код — сюда входит обновление состояния в React, перерасчёт реактивных зависимостей в Vue, срабатывание middleware в Redux. Если обработчик лезет в DOM синхронно, делает тяжёлые вычисления или запускает каскад пересчётов стилей — Processing Time растёт.
3. Presentation Delay — задержка отрисовки
После того как все обработчики отработали, браузеру нужно пересчитать стили, выполнить layout, композитинг и отрисовать обновлённый кадр. Казалось бы, рутина. Но на страницах с раздутым DOM (больше 1500 элементов) или навороченными CSS-анимациями этот этап легко съедает десятки миллисекунд.
Важный момент: понимание того, какая именно фаза является узким местом, определяет всю стратегию оптимизации. Я видел немало случаев, когда разработчики часами оптимизировали обработчики событий, а 80% задержки приходилось на Input Delay из-за тяжёлых сторонних скриптов. Не повторяйте эту ошибку — сначала диагностика, потом лечение.
Диагностика INP: находим виновника
Chrome DevTools: Performance Panel
Открываем Chrome DevTools (F12 или Ctrl+Shift+I), переходим на вкладку Performance, смотрим на раздел Live Metrics. Начинаем кликать по элементам страницы — DevTools будет записывать каждое взаимодействие в лог Interactions и подсвечивать самое медленное (это и есть наш кандидат на INP).
Для более детального анализа записываем трассировку: жмём кнопку записи, выполняем проблемное взаимодействие, останавливаем. В дорожке Interactions каждое взаимодействие разбито на три фазы. Наводим курсор — и всплывающая подсказка показывает точные цифры:
// Пример вывода DevTools для медленного взаимодействия
Interaction: pointerup (click)
Total Duration: 279 ms
├── Input Delay: 24 ms
├── Processing Duration: 191 ms ← основная проблема
└── Presentation Delay: 62 ms
Логика простая: если доминирует Processing Duration — копаем в обработчики событий. Input Delay зашкаливает — на главном потоке крутится какая-то фоновая задача. Presentation Delay великоват — DOM раздут или CSS перемудрён.
Long Animation Frames API (LoAF)
Long Animation Frames API (появился в Chrome 123) — это, по сути, Long Tasks API на стероидах. Он даёт куда больше контекста о том, что именно затормозило отрисовку:
- Какие конкретно скрипты выполнялись во время медленного кадра
- URL источника каждого скрипта (да, включая сторонние)
- Имя функции и позицию в исходном коде
- Сколько времени ушло на стили, layout и рендеринг
Вот как собирать данные LoAF в связке с INP прямо в продакшене:
import { onINP } from 'web-vitals/attribution';
onINP((metric) => {
const { value, attribution } = metric;
const { longAnimationFrameEntries } = attribution;
if (longAnimationFrameEntries?.length) {
const loaf = longAnimationFrameEntries[0];
console.log('INP Value:', value, 'ms');
console.log('Scripts during interaction:');
loaf.scripts.forEach((script) => {
console.log({
sourceURL: script.sourceURL,
functionName: script.sourceFunctionName,
duration: script.duration,
invoker: script.invoker,
});
});
}
});
Этот код покажет точное имя функции и файл, ответственный за медленное взаимодействие. Причём как ваш собственный код, так и сторонние скрипты — никто не спрячется.
Полевые данные: CrUX и Search Console
Лабораторные тесты — это хорошо, но они показывают лишь часть картины. В лабе вы проверяете ограниченный набор взаимодействий на мощной машине разработчика. А реальную картину даёт Chrome User Experience Report (CrUX) — агрегированные данные от настоящих пользователей Chrome. Загляните в PageSpeed Insights или Google Search Console → Core Web Vitals, чтобы увидеть ваш INP на 75-м перцентиле.
Оптимизация Input Delay: освобождаем главный поток
scheduler.yield() — современный способ разбить длинные задачи
scheduler.yield() — относительно новый браузерный API (Chrome 125+), созданный специально для улучшения отзывчивости. И вот в чём его главное преимущество: в отличие от старого доброго setTimeout(fn, 0), который ставит продолжение задачи в конец очереди, scheduler.yield() помещает его в начало. То есть ваш код продолжит работу сразу после обработки пользовательского ввода, а не после всех остальных задач.
// Обработка большого массива данных с yield
async function processLargeDataset(items) {
const CHUNK_SIZE = 50;
for (let i = 0; i < items.length; i += CHUNK_SIZE) {
const chunk = items.slice(i, i + CHUNK_SIZE);
chunk.forEach(item => heavyTransform(item));
// Отдаём управление браузеру — он обработает
// пользовательские взаимодействия, если они есть
await scheduler.yield();
}
}
// Кроссбраузерный fallback
function yieldToMain() {
if (globalThis.scheduler?.yield) {
return scheduler.yield();
}
return new Promise(resolve => setTimeout(resolve, 0));
}
Нюанс: scheduler.yield() пока не поддерживается в Safari (ну кто бы удивился). Так что fallback на setTimeout обязателен.
Стратегия yield: где именно вставлять точки уступки
Не нужно фанатично ставить yield после каждой строки. Цель проще — не допускать задач длиннее 50 мс. Откройте Performance panel в DevTools, найдите длинные задачи и вставляйте yield между логическими блоками:
async function handleFormSubmit(formData) {
// Критический блок 1: валидация (быстро)
const errors = validateForm(formData);
if (errors.length) {
showErrors(errors);
return;
}
// Визуальная обратная связь — сразу
showLoadingSpinner();
// Уступаем перед тяжёлой работой
await yieldToMain();
// Критический блок 2: подготовка данных (может быть долго)
const payload = await preparePayload(formData);
// Уступаем снова перед сетевым запросом
await yieldToMain();
// Критический блок 3: отправка
const response = await submitToServer(payload);
handleResponse(response);
}
Оптимизация Processing Time: быстрые обработчики событий
Разделение критического и некритического кода
Принцип на самом деле прост: в обработчике события должен выполняться только код, необходимый для визуального обновления. Всё остальное — аналитика, логирование, фоновая синхронизация — откладывается. Звучит очевидно, но посмотрите на свой код — скорее всего, там найдётся пара-тройка обработчиков, которые делают слишком много.
button.addEventListener('click', () => {
// ✅ Критический код: обновляем UI немедленно
updateCartBadge(+1);
showAddedToCartAnimation();
// ❌ НЕ критический: аналитика, синхронизация
// Откладываем на время простоя браузера
if ('requestIdleCallback' in window) {
requestIdleCallback(() => {
trackEvent('add_to_cart', { productId: 123 });
syncCartWithServer();
}, { timeout: 2000 });
} else {
setTimeout(() => {
trackEvent('add_to_cart', { productId: 123 });
syncCartWithServer();
}, 0);
}
});
requestIdleCallback запланирует выполнение на момент простоя главного потока, а timeout: 2000 гарантирует, что код отработает не позднее чем через 2 секунды — даже если браузер занят под завязку.
Debounce для частых событий
События типа input, scroll, resize могут срабатывать десятки раз в секунду. Если каждое срабатывание запускает что-то тяжёлое — фильтрацию списка, поисковый запрос — получаем каскад длинных задач, которые намертво блокируют остальные взаимодействия:
function debounce(fn, ms) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), ms);
};
}
// Поиск срабатывает через 300 мс после последнего нажатия
const searchInput = document.getElementById('search');
searchInput.addEventListener('input', debounce((e) => {
performSearch(e.target.value);
}, 300));
// Для событий, где нужна немедленная реакция,
// используйте throttle вместо debounce
function throttle(fn, ms) {
let lastCall = 0;
return (...args) => {
const now = Date.now();
if (now - lastCall >= ms) {
lastCall = now;
fn(...args);
}
};
}
Отмена устаревшей работы с AbortController
Когда пользователь быстро кликает несколько раз или печатает в поисковой строке, предыдущие операции становятся бесполезными. Зачем ждать их завершения? Лучше отменить:
let currentController = null;
async function handleSearchInput(query) {
// Отменяем предыдущий поиск
currentController?.abort();
currentController = new AbortController();
const { signal } = currentController;
// Визуальная обратная связь — мгновенно
showSearchSpinner();
try {
// Тяжёлая обработка по частям с возможностью отмены
const results = await processInChunks(query, signal);
if (!signal.aborted) {
renderSearchResults(results);
}
} catch (e) {
if (e.name !== 'AbortError') throw e;
}
}
async function processInChunks(query, signal) {
const allItems = getSearchableItems();
const results = [];
const CHUNK = 100;
for (let i = 0; i < allItems.length; i += CHUNK) {
if (signal.aborted) return results;
const chunk = allItems.slice(i, i + CHUNK);
results.push(...chunk.filter(item => matchesQuery(item, query)));
await yieldToMain();
}
return results;
}
Оптимизация Presentation Delay: быстрая отрисовка
Уменьшение размера DOM
Каждый элемент в DOM — это дополнительное время на пересчёт стилей и layout. Google рекомендует не превышать 1400 элементов на странице. Если у вас интернет-магазин с сотнями карточек товаров (а это очень типичная ситуация), без виртуализации не обойтись:
- Для React:
react-windowили@tanstack/react-virtual - Для Vue:
vue-virtual-scroller - Для vanilla JS:
content-visibility: autoв CSS
Избегайте layout thrashing
Layout thrashing — ситуация, когда код чередует чтение и запись DOM-свойств, вынуждая браузер пересчитывать layout на каждом чтении. Выглядит невинно, а на деле убивает производительность:
// ❌ Layout thrashing: каждый offsetHeight вызывает
// принудительный layout
items.forEach(item => {
const height = item.offsetHeight; // чтение → layout!
item.style.height = height * 2 + 'px'; // запись
});
// ✅ Batch reads, then batch writes
const heights = items.map(item => item.offsetHeight); // все чтения
items.forEach((item, i) => {
item.style.height = heights[i] * 2 + 'px'; // все записи
});
CSS-анимации на свойствах композитора
Анимируйте только transform и opacity — они обрабатываются на GPU без пересчёта layout. Всё остальное — width, height, top, left, margin, padding — это гарантированный layout при каждом кадре анимации. Просто не делайте так.
Web Workers: перенос тяжёлых вычислений из главного потока
Если задача требует серьёзных вычислений — сортировка больших массивов, обработка изображений, парсинг CSV — самое время подумать о Web Workers. Worker работает в отдельном потоке и физически не может заблокировать главный:
// main.js
const worker = new Worker('/workers/data-processor.js');
document.getElementById('sort-btn').addEventListener('click', () => {
// UI обновляется мгновенно
showLoadingState();
// Тяжёлая сортировка — в Worker
worker.postMessage({ action: 'sort', data: largeDataset });
});
worker.addEventListener('message', (e) => {
hideLoadingState();
renderSortedData(e.data.sorted);
});
// workers/data-processor.js
self.addEventListener('message', (e) => {
if (e.data.action === 'sort') {
const sorted = e.data.data.sort((a, b) => {
// Тяжёлая сортировка по нескольким полям
return complexCompare(a, b);
});
self.postMessage({ sorted });
}
});
Web Workers отлично подходят для всего, что не требует мгновенного результата: фильтрация данных, криптография, обработка canvas, форматирование дат в больших таблицах. По опыту, перенос даже одной тяжёлой операции в Worker часто даёт заметный прирост по INP.
Фреймворк-специфичные оптимизации
React: startTransition и useDeferredValue
React 18+ из коробки умеет разделять срочные и несрочные обновления. Если вы ещё не используете эти API — настоятельно рекомендую начать:
import { useState, startTransition, useDeferredValue } from 'react';
function ProductFilter({ products }) {
const [query, setQuery] = useState('');
const [filteredProducts, setFilteredProducts] = useState(products);
// Ввод текста — срочное обновление
// Фильтрация списка — несрочное
const handleInput = (e) => {
const value = e.target.value;
setQuery(value); // Срочно: обновить input
startTransition(() => {
// React может прервать эту работу,
// если придёт новое взаимодействие
setFilteredProducts(
products.filter(p =>
p.name.toLowerCase().includes(value.toLowerCase())
)
);
});
};
// Альтернативный подход: useDeferredValue
const deferredQuery = useDeferredValue(query);
const isStale = query !== deferredQuery;
return (
<div>
<input value={query} onChange={handleInput} />
<div style={{ opacity: isStale ? 0.7 : 1 }}>
<ProductList products={filteredProducts} />
</div>
</div>
);
}
Vue: nextTick и watchEffect с flush
import { ref, watchEffect, nextTick } from 'vue';
const query = ref('');
const results = ref([]);
// Обработчик с отложенной фильтрацией
watchEffect(async () => {
const q = query.value;
// Ждём следующий тик рендеринга
await nextTick();
// Тяжёлая фильтрация не блокирует ввод
results.value = allProducts.filter(p =>
p.name.toLowerCase().includes(q.toLowerCase())
);
}, { flush: 'post' }); // Выполнять после обновления DOM
Сравнительная таблица стратегий оптимизации INP
| Стратегия | Фаза INP | Эффект | Сложность |
|---|---|---|---|
scheduler.yield() |
Input Delay | Высокий | Низкая |
| Defer сторонних скриптов | Input Delay | Высокий | Низкая |
| Debounce/throttle обработчиков | Processing Time | Средний | Низкая |
requestIdleCallback для аналитики |
Processing Time | Средний | Низкая |
| Web Workers | Processing Time | Высокий | Средняя |
React startTransition |
Processing Time | Высокий | Низкая |
| AbortController для отмены | Processing Time | Средний | Средняя |
| Виртуализация DOM | Presentation Delay | Высокий | Средняя |
| Batch DOM reads/writes | Presentation Delay | Средний | Низкая |
GPU-анимации (transform/opacity) |
Presentation Delay | Средний | Низкая |
Чеклист: быстрые победы для улучшения INP
Итак, если вы хотите действовать прямо сейчас — вот порядок действий от простого к сложному:
- Откройте PageSpeed Insights и проверьте текущий INP из полевых данных CrUX
- В Chrome DevTools → Performance → Live Metrics найдите самое медленное взаимодействие
- Определите доминирующую фазу (Input Delay, Processing, Presentation)
- Добавьте
deferилиasyncко всем некритическим скриптам в<head> - Перенесите аналитику и трекинг в
requestIdleCallback - Добавьте
scheduler.yield()(с fallback) в обработчики, работающие дольше 50 мс - Используйте debounce (300 мс) для поисковых полей и фильтров
- Проверьте размер DOM — если больше 1400 элементов, внедряйте виртуализацию или
content-visibility: auto - Удалите или замените facade-паттерном тяжёлые сторонние виджеты (чат-боты, карусели)
- Повторите замеры — целевое значение: INP ≤ 200 мс на 75-м перцентиле
Часто задаваемые вопросы
Чем INP отличается от FID и почему FID больше не используется?
FID измеряла задержку только первого взаимодействия и только фазу Input Delay — Processing Time и Presentation Delay она полностью игнорировала. На практике это означало, что страница могла получить отличный FID, но при этом тормозить на всём остальном: выпадающие меню, фильтры, карусели. INP оценивает все взаимодействия и учитывает полную цепочку от ввода до отрисовки — гораздо более честная картина.
Какой показатель INP считается хорошим в 2026 году?
Google выделяет три порога: до 200 мс — хороший (good), 200–500 мс — требует улучшения (needs improvement), более 500 мс — плохой (poor). Важно: измерение идёт по 75-му перцентилю всех пользовательских взаимодействий. То есть 75% взаимодействий должны укладываться в 200 мс. Если проходите — Google учтёт это как положительный сигнал ранжирования.
Как scheduler.yield() отличается от setTimeout(fn, 0)?
Оба метода «разрывают» длинную задачу и возвращают управление браузеру. Ключевая разница — в приоритете продолжения. setTimeout(fn, 0) ставит продолжение в конец очереди задач, и другие задачи (сторонние скрипты, таймеры) могут вклиниться. scheduler.yield() ставит продолжение в начало, обеспечивая быстрое возобновление сразу после пользовательских событий. На практике это означает меньшее общее время выполнения вашего кода и более предсказуемое поведение.
Влияет ли INP на SEO-позиции сайта?
Да. INP входит в Core Web Vitals, которые являются сигналом ранжирования Google с 2021 года. По данным Google, сайты, проходящие все три метрики Core Web Vitals, показывают на 24% ниже показатель отказов. INP — не единственный фактор, но в конкурентных нишах может стать тем самым решающим преимуществом.
Как улучшить INP, если проблема в сторонних скриптах?
Сторонние скрипты — одна из самых частых причин плохого INP. Что делать: (1) загружайте их с defer или async; (2) используйте facade-паттерн — вместо полного виджета чата покажите статическую кнопку, а настоящий виджет грузите только по клику; (3) перенесите аналитику в requestIdleCallback или Web Worker через Partytown; (4) регулярно аудируйте через Long Animation Frames API — так вы будете точно знать, какие скрипты блокируют главный поток и насколько сильно.