Оптимизация INP в 2026: как добиться отзывчивости за 200 мс

INP — самая проблемная метрика Core Web Vitals: 43% сайтов не укладываются в 200 мс. Разбираем три фазы INP, диагностику через DevTools и LoAF API, а также scheduler.yield(), Web Workers и подходы для React и Vue.

Почему 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

Итак, если вы хотите действовать прямо сейчас — вот порядок действий от простого к сложному:

  1. Откройте PageSpeed Insights и проверьте текущий INP из полевых данных CrUX
  2. В Chrome DevTools → Performance → Live Metrics найдите самое медленное взаимодействие
  3. Определите доминирующую фазу (Input Delay, Processing, Presentation)
  4. Добавьте defer или async ко всем некритическим скриптам в <head>
  5. Перенесите аналитику и трекинг в requestIdleCallback
  6. Добавьте scheduler.yield() (с fallback) в обработчики, работающие дольше 50 мс
  7. Используйте debounce (300 мс) для поисковых полей и фильтров
  8. Проверьте размер DOM — если больше 1400 элементов, внедряйте виртуализацию или content-visibility: auto
  9. Удалите или замените facade-паттерном тяжёлые сторонние виджеты (чат-боты, карусели)
  10. Повторите замеры — целевое значение: 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 — так вы будете точно знать, какие скрипты блокируют главный поток и насколько сильно.

Об авторе Editorial Team

Our team of expert writers and editors.