12 марта 2024 года метрика First Input Delay в наборе Core Web Vitals была выведена из использования и заменена на Interaction to Next Paint, сокращённо INP. О будущей замене команда Chrome предупредила ещё в апреле 2023 года, чтобы разработчики успели промерить страницы и починить самое тяжёлое. К 2026 году INP уже два года формирует оценку сайта в отчёте Core Web Vitals, и красная зона в этом отчёте оборачивается потерянными позициями и покупателями. Метрика без снисхождения фиксирует почти худший отклик страницы на действия посетителя и не прощает тяжёлому JavaScript длинных синхронных задач в обработчиках событий. С виду это просто смена аббревиатуры, по сути сменился вопрос, который задают странице: раньше спрашивали, как быстро она начинает думать после первого клика, теперь спрашивают, как быстро она отвечает всегда. Ниже разбор механики INP, порогов 200 и 500 миллисекунд и приёмов, которые возвращают интерфейсу скорость на реальном коде: разбиение кода, динамический import, веб-воркеры, дробление обработчиков и метод scheduler.yield.

Метрика Interaction to Next Paint измеряет полный цикл реакции страницы

Нажатие кнопки и её видимый отклик для браузера два разных события, между которыми умещается целая история. Секундомер INP стартует в момент действия человека: клик мышью, касание экрана, нажатие клавиши. Финиш фиксируется, когда браузер отрисовывает следующий кадр, в котором уже виден результат. Между этими точками всегда три фазы: ожидание до старта обработчиков, называемое input delay, исполнение самих обработчиков и подготовка кадра, presentation delay. Метрика складывает все три, и это справедливо: посетителю всё равно, на каком этапе страница задумалась, кнопку он уже нажал, а отклика всё нет.

Значение за визит берётся почти по худшему взаимодействию. Если взаимодействий случилось больше 50, браузер берёт примерно 98-й перцентиль, и единичный аномальный выброс уже не портит итог. Оценка страницы в полевых данных считается на 75-м перцентиле визитов за 28 дней, причём мобильные и настольные устройства учитываются раздельно. Пороги жёсткие: до 200 мс засчитывается "хорошо", диапазон от 200 до 500 мс получает пометку "требует улучшения", всё выше 500 мс признаётся "плохо". Прокрутка в зачёт не идёт, поскольку считается непрерывным действием: INP видит клики, тапы и клавиатуру, то есть действия, на которые страница обязана ответить видимым изменением. Визит без единого взаимодействия вообще не даёт странице значения INP.

First Input Delay замечала только первое взаимодействие и не видела долгие обработчики

Прежняя метрика фиксировала одну величину: задержку до запуска обработчика первого взаимодействия. Ни исполнение обработчика, ни время до следующей отрисовки в неё не входили. Первый клик попадает в спокойный момент, задержка 25 мс, формально зелёный статус. Через минуту посетитель раскрывает фильтр каталога, обработчик синхронно перебирает 12 000 записей шестьсот миллисекунд, интерфейс застывает, но отчёт по-прежнему сияет зелёным. Пороги старой метрики к тому же были мягче: 100 мс для "хорошо" и 300 мс для "плохо".

Отсюда и замена. INP закрывает обе слепые зоны сразу: он видит все взаимодействия визита и меряет полный путь до кадра с результатом. Спрятаться за удачным первым кликом больше не получится, и медленный обработчик отражается в цифрах ровно так, как ощущается живым человеком. Для команды разработки это ценнее средних значений: оценку формируют худшие визиты, то есть именно те моменты, когда уставший посетитель тянется закрыть вкладку.

Долгие задачи JavaScript свыше 50 миллисекунд блокируют главный поток

У вкладки один главный поток, и на нём происходит всё: исполнение скриптов, пересчёт стилей и раскладки, отрисовка кадров. Пока тянется длинная задача, события копятся в очереди, и свежий клик ждёт освобождения потока ещё до запуска обработчика. Задачу длиннее 50 мс принято считать долгой. Почему именно 50 мс, а не 30 и не 100? Классическая модель RAIL отводила на реакцию около 100 мс, из них браузеру нужны кадры на отрисовку результата, поэтому на код обработчика остаётся примерно половина бюджета.

В Chrome DevTools на панели Performance долгие задачи выделены прямо на треке Main, а соседний ряд Interactions показывает задержку каждого действия. В поле работает штатный PerformanceObserver с типом longtask: код ниже пишет в консоль каждую задачу свыше 50 мс.

const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log("Долгая задача: " + entry.duration.toFixed(0) + " мс");
  }
});
observer.observe({ type: "longtask", buffered: true });

Появился и более точный прибор: Long Animation Frames API, доступный в Chrome начиная с версии 123, показывает не только длительность тяжёлого кадра, но и конкретный скрипт с функцией, которые его вызвали. Для лабораторных прогонов остаётся Total Blocking Time: Lighthouse суммирует блокирующие части долгих задач, то есть всё, что в каждой задаче сверх 50 мс, и именно TBT предсказывает будущий полевой INP лучше других лабораторных чисел.

Разбиение кода и динамический import сокращают стартовый пакет JavaScript

Первый способ снизить давление на поток прост: исполнять меньше. К первому экрану обычно нужно меньшинство кода, а тяжёлые библиотеки диаграмм, редакторов и карт живут на дальних экранах. Code splitting делит бандл на части по маршрутам и компонентам, а динамический import подтягивает нужную часть в момент, когда она действительно понадобилась. Во фреймворках этот приём спрятан в готовых абстракциях вроде React.lazy, в сборщиках webpack и Vite динамические импорты автоматически превращаются в отдельные файлы.

button.addEventListener("click", async () => {
  const charts = await import("./modules/charts.js");
  charts.render(container, data);
});

Тут есть подводный камень. Загрузка модуля прямо по клику на медленной сети добавляет сотни миллисекунд к самому взаимодействию, и оптимизация оборачивается новой проблемой. Надёжнее прогревать кэш заранее: в простое через requestIdleCallback или scheduler.postTask с фоновым приоритетом, при наведении на кнопку, сразу после первого кадра.

Цифры условного проекта выглядят так: бандл интернет-магазина весил 1,7 МБ, после выноса редактора диаграмм и карт из стартовой поставки вес упал до 540 КБ, время разбора и компиляции скриптов сократилось примерно втрое, а долгие задачи на старте страницы исчезли полностью. Рабочая связка приёмов: tree shaking для мёртвого кода, современные цели сборки уровня ES2017 и отказ от библиотек, таскающих полные наборы локалей.

Веб-воркеры и отложенная инициализация убирают тяжёлые вычисления из главного потока

Воркер живёт в отдельном потоке со своей очередью задач и не мешает главному потоку рисовать кадры. Туда переезжают чистые вычисления: фильтрация и сортировка больших массивов, поиск по каталогу, парсинг объёмных JSON, агрегация чисел для отчётов. DOM из воркера недоступен, поэтому архитектура получается ясной: данные считает воркер, разметку рисует главный поток.

const worker = new Worker("/js/catalog-worker.js");

worker.onmessage = (event) => {
  renderList(event.data.items);
};

filterButton.addEventListener("click", () => {
  showSpinner();
  worker.postMessage({ action: "filter", query: input.value });
});

Передача большого объекта через structured clone копирует данные целиком, а передача ArrayBuffer как transferable отдаёт буфер воркеру без копирования. На сотнях тысяч записей разница между копированием и сменой владельца измеряется десятками миллисекунд.

Отложенная инициализация закрывает вторую половину проблемы. Тяжёлые виджеты вроде карты или видеоплеера не обязаны стартовать вместе со страницей: карту разумно грузить при первом наведении на секцию с контактами, плеер при первом клике по постеру. Сторонним скриптам тоже задают отложенный старт: атрибуты async и defer, загрузка библиотек аналитики после первого взаимодействия.

Дробление обработчиков и метод scheduler.yield возвращают браузеру время на кадр

Даже компактный бандл не спасает от длинных циклов в самих обработчиках: пересчёт корзины, подготовка отчёта на 40 000 строк, синхронный поиск по словарю. Ответ на это дробление. Синхронно выполняется только то, что даёт мгновенный видимый отклик: подсветка строки, смена вида кнопки, индикатор загрузки. Остальное делится на порции и исполняется с уступкой потока между ними, чтобы браузер успевал рисовать кадры и забирать из очереди новые события.

async function recalcRows(rows) {
  const chunk = 500;
  for (let i = 0; i < rows.length; i += chunk) {
    updateFragment(rows.slice(i, i + chunk));
    if (i + chunk < rows.length) {
      await yieldToBrowser();
    }
  }
}

function yieldToBrowser() {
  if ("scheduler" in window && "yield" in scheduler) {
    return scheduler.yield();
  }
  return new Promise((resolve) => setTimeout(resolve, 0));
}

Тонкий момент, о котором часто умалчивают. Простой setTimeout уже улучшает цифру конкретного взаимодействия, потому что обработчик быстро отдаёт поток и кадр рисуется. Но отложенная работа не исчезает: она задерживает следующие клики, и пострадавшим в отчёте окажется уже другое действие. Настоящий эффект даёт деление на порции до 50 мс. Метод scheduler.yield, доступный в Chrome начиная с версии 129, решает и вопрос приоритетов: продолжение задачи не уезжает в конец очереди и не конкурирует с обработкой новых кликов, тогда как таймер ставит продолжение позади всех.

И ещё одна привычка экономит десятки миллисекунд: в обработчиках нельзя чередовать чтение и запись свойств, влияющих на раскладку. Каждый заход вида "прочитал offsetHeight, тут же записал стиль" заставляет браузер пересчитать раскладку заново. Правильный порядок: сначала собрать все чтения, затем выполнить все записи.

Полевые замеры до и после оптимизации и порядок работы со страницей

Условный пример: каталог электроники, где клик по фильтру запускал долгую задачу на 580 мс, потому что пересчёт и рендер 12 000 записей шли одним куском. Полевой INP на 75-м перцентиле держался на отметке 640 мс, оценка "плохо" по обоим типам устройств. Что изменили: фильтрацию и сортировку перенесли в воркер, рендер списка стали дробить порциями по 50 записей с уступкой потока, тяжёлый скрипт диаграмм ушёл из стартового бандла в динамический import с прогревом кэша. Обработчик клика сжался до 45 мс: подсветка, индикатор и одно сообщение воркеру. Через 28 дней полевой замер показал INP 170 мс и зелёную оценку по мобильному трафику, выигрыш составил 470 мс на каждом тяжёлом визите.

Клавиатурный случай считается похожим образом. Строка поиска синхронно перебирала 40 000 названий на каждое нажатие клавиши, задержка достигала 210 мс, INP сидел на 240 мс. После переноса перебора в воркер с кэшированием последнего результата отклик клавиши уложился в 40 мс, а INP опустился до 90 мс.

Для собственных замеров используют связку из трёх инструментов. Отчёт Core Web Vitals в Search Console показывает полевые данные за 28 дней и группирует страницы по шаблонам адресов. PageSpeed Insights совмещает те же полевые данные с лабораторным прогоном Lighthouse и его TBT. Библиотека web-vitals добавляет собственную телеметрию прямо на сайт, а версия attribution подсказывает, какой элемент и какой тип взаимодействия тормозили.

import { onINP } from "web-vitals/attribution";

onINP((metric) => {
  const info = metric.attribution;
  sendToAnalytics({
    value: Math.round(metric.value),
    rating: metric.rating,
    target: info.eventTarget,
    type: info.interactionType
  });
});

Порядок выхода из красной зоны складывается из пяти шагов:
1. собрать полевые данные INP за 28 дней и выписать шаблоны адресов с оценкой "плохо";
2. воспроизвести медленные взаимодействия в DevTools с троттлингом процессора и найти задачи свыше 50 мс;
3. вынести чистые вычисления в воркер и убрать из стартового бандла всё, что не нужно первому экрану;
4. раздробить оставшиеся обработчики на порции до 50 мс с уступкой потока через scheduler.yield;
5. повторить полевой замер через 28 дней и убедиться, что 75-й перцентиль уложился в 200 мс.

К 2026 году пороги не сдвинулись ни на миллисекунду: 200 мс для "хорошо", 500 мс для красной зоны. INP остаётся в наборе Core Web Vitals, а инструментарий вокруг него вырос: Long Animation Frames API указывает виновника тяжёлого кадра, scheduler.yield даёт культурный способ уступать поток, атрибуция в web-vitals находит слабый элемент. Метрика в итоге измеряет то же, что чувствует человек: нажал и сразу увидел. Тяжёлый JavaScript не отменяет этого правила, а превращает его в инженерную задачу с конкретными числами, готовыми приёмами и понятным порядком действий.