1. Введение
Интерактивная демонстрация и практическая работа, посвященные показателю Interaction to Next Paint (INP).
Требования
- Знание HTML и JavaScript.
- Рекомендуем ознакомиться с документацией по INP.
Что вы узнаете
- Как действия пользователей и ваша реакция на них влияют на скорость отклика страницы.
- Как уменьшить и устранить задержки, чтобы пользователям было удобно.
Что вам потребуется
- Компьютер с возможностью клонировать код из GitHub и выполнять команды npm.
- Текстовый редактор.
- Последняя версия Chrome, чтобы работали все измерения взаимодействий.
2. Подготовка
Как получить и запустить код
Код находится в хранилище web-vitals-codelabs.
- Клонируйте репозиторий в терминале:
git clone https://github.com/GoogleChromeLabs/web-vitals-codelabs.git. - Перейдите в клонированный каталог:
cd web-vitals-codelabs/understanding-inp. - Установите зависимости:
npm ci - Запустите веб-сервер:
npm run start. - Откройте в браузере страницу http://localhost:5173/understanding-inp/.
Обзор приложения
В верхней части страницы находится счетчик Score (Счет) и кнопка Increment (Увеличить). Классическая демонстрация реактивности и отзывчивости!

Под кнопкой указаны четыре показателя:
- INP – текущий показатель INP, который обычно соответствует худшему взаимодействию.
- Взаимодействие – оценка последнего взаимодействия.
- FPS – количество кадров в секунду в основном потоке страницы.
- Таймер: анимация работающего таймера, которая помогает визуализировать задержки.
Записи FPS и Timer не нужны для измерения взаимодействий. Они добавлены только для того, чтобы было проще визуализировать адаптивность.
Попробовать
Нажмите кнопку Increment (Увеличить) и посмотрите, как увеличится счет. Меняются ли значения INP и Interaction с каждым шагом?
INP – это время, которое проходит с момента взаимодействия пользователя со страницей до момента, когда на ней появляется обновленный контент.
3. Как измерить взаимодействия с помощью инструментов разработчика Chrome
Откройте инструменты разработчика, выбрав Дополнительные инструменты > Инструменты разработчика, нажав правой кнопкой мыши на странице и выбрав Проверить или используя сочетание клавиш.
Перейдите на панель Производительность, которая используется для измерения взаимодействий.

Затем запишите взаимодействие на панели "Производительность".
- Нажмите кнопку записи.
- Взаимодействуйте со страницей (нажмите кнопку Increment).
- Остановить запись.
На временной шкале вы увидите дорожку Interactions (Взаимодействия). Разверните его, нажав на треугольник слева.

Появятся два взаимодействия. Увеличьте масштаб второго, прокрутив колесико мыши или удерживая клавишу W.

Наведите указатель мыши на взаимодействие. Вы увидите, что оно было быстрым, длительность обработки была нулевой, а задержка ввода и задержка отрисовки – минимальными. Точные значения зависят от скорости вашего компьютера.
4. Прослушиватели событий, которые выполняются долго
Откройте файл index.js и раскомментируйте функцию blockFor в прослушивателе событий.
Посмотреть полный код: click_block.html
button.addEventListener('click', () => {
blockFor(1000);
score.incrementAndUpdateUI();
});
Сохраните файл. Сервер обнаружит изменение и обновит страницу.
Попробуйте снова выполнить действие на странице. Теперь взаимодействия будут заметно медленнее.
Трассировка производительности
Сделайте ещё одну запись на панели "Производительность", чтобы посмотреть, как это выглядит.

То, что раньше было коротким взаимодействием, теперь занимает целую секунду.
Если навести указатель мыши на взаимодействие, можно заметить, что время почти полностью уходит на обработку, то есть на выполнение обратных вызовов прослушивателя событий. Поскольку блокирующий вызов blockFor полностью находится в прослушивателе событий, время тратится именно на него.
5. Эксперимент: продолжительность обработки
Попробуйте по-разному перераспределить работу прослушивателя событий, чтобы посмотреть, как это повлияет на INP.
Сначала обновите интерфейс
Что произойдет, если поменять порядок вызовов JS: сначала обновить интерфейс, а затем заблокировать?
Посмотреть полный код: ui_first.html
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
blockFor(1000);
});
Вы заметили, что интерфейс появился раньше? Влияет ли порядок на показатели INP?
Попробуйте выполнить трассировку и изучить взаимодействие, чтобы понять, есть ли различия.
Отдельные слушатели
Что если перенести работу в отдельный прослушиватель событий? Обновите интерфейс в одном прослушивателе событий и заблокируйте страницу в другом.
Посмотреть полный код: two_click.html
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
});
button.addEventListener('click', () => {
blockFor(1000);
});
Как это выглядит на панели производительности сейчас?
Разные типы событий
Большинство взаимодействий вызывают множество типов событий: от событий указателя или клавиши до наведения, фокуса/размытия и синтетических событий, таких как beforechange и beforeinput.
На многих реальных страницах есть прослушиватели для разных событий.
Что произойдет, если изменить типы событий для прослушивателей событий? Например, заменить один из прослушивателей событий click на pointerup или mouseup?
Посмотреть полный код: diff_handlers.html
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
});
button.addEventListener('pointerup', () => {
blockFor(1000);
});
Интерфейс не обновлен
Что произойдет, если удалить вызов для обновления пользовательского интерфейса из прослушивателя событий?
Посмотреть полный код: no_ui.html
button.addEventListener('click', () => {
blockFor(1000);
// score.incrementAndUpdateUI();
});
6. Результаты эксперимента с продолжительностью обработки
Трассировка производительности: сначала обновите интерфейс
Посмотреть полный код: ui_first.html
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
blockFor(1000);
});
Посмотрев запись нажатия кнопки на панели "Производительность", вы увидите, что результаты не изменились. Хотя обновление интерфейса было запущено до блокирующего кода, браузер фактически не обновил то, что было нарисовано на экране, до тех пор, пока не завершился прослушиватель событий, а это означает, что на завершение взаимодействия по-прежнему ушло чуть больше секунды.

Трассировка производительности: отдельные прослушиватели
Посмотреть полный код: two_click.html
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
});
button.addEventListener('click', () => {
blockFor(1000);
});
Опять же, функционально разницы нет. Взаимодействие по-прежнему занимает целую секунду.
Если вы увеличите масштаб взаимодействия по клику, то увидите, что в результате события click действительно вызываются две разные функции.
Как и ожидалось, первая задача (обновление интерфейса) выполняется невероятно быстро, а вторая занимает целую секунду. Однако в сумме их эффекты приводят к тому, что взаимодействие с конечным пользователем происходит медленно.

Трассировка производительности: разные типы событий
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
});
button.addEventListener('pointerup', () => {
blockFor(1000);
});
Эти результаты очень похожи. Взаимодействие по-прежнему занимает целую секунду. Единственное отличие заключается в том, что более короткий прослушиватель click, который только обновляет интерфейс, теперь запускается после блокирующего прослушивателя pointerup.

Отслеживание производительности: интерфейс не обновлен
Посмотреть полный код: no_ui.html
button.addEventListener('click', () => {
blockFor(1000);
// score.incrementAndUpdateUI();
});
- Счет не обновляется, но страница по-прежнему работает.
- Анимация, эффекты CSS, действия веб-компонентов по умолчанию (ввод данных в форму), ввод текста и выделение текста продолжают обновляться.
В этом случае кнопка переходит в активное состояние и обратно при нажатии, что требует отрисовки браузером, а значит, INP все равно есть.
Поскольку прослушиватель событий заблокировал основной поток на секунду, не давая странице отрисоваться, взаимодействие все равно занимает целую секунду.
Запись на панели "Производительность" показывает, что взаимодействие практически идентично предыдущим.

Заказ навынос
Любой код, выполняемый в любом прослушивателе событий, задерживает взаимодействие.
- Это касается прослушивателей, зарегистрированных из разных скриптов, и кода фреймворка или библиотеки, который выполняется в прослушивателях, например обновления состояния, которое запускает рендеринг компонента.
- Это касается не только вашего кода, но и всех сторонних скриптов.
Это распространенная проблема.
Наконец, то, что ваш код не запускает отрисовку, не означает, что отрисовка не будет ждать завершения медленных прослушивателей событий.
7. Эксперимент: задержка ввода
Что насчет долго выполняющегося кода вне прослушивателей событий? Пример:
- Если у вас есть медленно загружающийся элемент
<script>, который случайным образом блокирует страницу во время загрузки. - Вызов API, например
setInterval, который периодически блокирует страницу?
Попробуйте удалить blockFor из прослушивателя событий и добавить его в setInterval():
Посмотреть полный код: input_delay.html
setInterval(() => {
blockFor(1000);
}, 3000);
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
});
Что происходит
8. Результаты эксперимента с задержкой ввода
Посмотреть полный код: input_delay.html
setInterval(() => {
blockFor(1000);
}, 3000);
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
});
Если пользователь нажмет кнопку во время выполнения задачи setInterval, взаимодействие будет долгим, даже если в нем самом не будет блокирующих задач.
Такие периоды часто называют длительными задачами.
Наведя указатель мыши на взаимодействие в DevTools, вы увидите, что время взаимодействия теперь в основном связано с задержкой ввода, а не с продолжительностью обработки.

Обратите внимание, что это не всегда влияет на взаимодействия. Если вы не нажмете, когда задача выполняется, вам может повезти. Такие "случайные" чихи могут стать кошмаром для отладки, когда они лишь иногда вызывают проблемы.
Один из способов отслеживать такие проблемы – измерять длительные задачи (или длительные кадры анимации) и общее время блокировки.
9. Медленная презентация
До сих пор мы рассматривали производительность JavaScript через задержку ввода или прослушиватели событий, но что ещё влияет на рендеринг следующей отрисовки?
Обновление страницы с ресурсоемкими эффектами.
Даже если страница обновляется быстро, браузеру все равно может потребоваться много ресурсов для отрисовки.
В основном потоке:
- фреймворки пользовательского интерфейса, которым нужно отображать обновления после изменения состояния;
- Изменения DOM или переключение многих ресурсоемких селекторов запросов CSS могут вызвать множество стилей, макетов и отрисовок.
Вне основного потока:
- Использование CSS для эффектов графического процессора
- Добавление очень больших изображений с высоким разрешением
- Использование SVG/Canvas для отрисовки сложных сцен

Вот несколько примеров:
- Сайт SPA, который перестраивает всю DOM после нажатия на ссылку, не делая паузы для предоставления первоначальной визуальной обратной связи.
- Поисковая страница, которая предлагает сложные фильтры поиска с динамическим пользовательским интерфейсом, но для этого запускает дорогостоящие прослушиватели.
- Переключатель тёмной темы, который меняет стиль и макет всей страницы.
10. Эксперимент: задержка вывода ответа на экран
Устройство "requestAnimationFrame" работает медленно
Давайте смоделируем длительную задержку показа с помощью requestAnimationFrame() API.
Переместите вызов blockFor в обратный вызов requestAnimationFrame, чтобы он выполнялся после возврата прослушивателя событий:
Посмотреть полный код: presentation_delay.html
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
requestAnimationFrame(() => {
blockFor(1000);
});
});
Что происходит
11. Результаты эксперимента с задержкой вывода ответа на экран
Посмотреть полный код: presentation_delay.html
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
requestAnimationFrame(() => {
blockFor(1000);
});
});
Взаимодействие по-прежнему длится секунду. Что же произошло?
requestAnimationFrame запрашивает обратный вызов до следующей отрисовки. Поскольку INP измеряет время от взаимодействия до следующей отрисовки, blockFor(1000) в requestAnimationFrame продолжает блокировать следующую отрисовку на целую секунду.

Однако обратите внимание на две вещи:
- При наведении указателя мыши вы увидите, что все время взаимодействия теперь тратится на задержку показа, поскольку блокировка основного потока происходит после возврата прослушивателя событий.
- Корнем активности основного потока больше не является событие клика, а "Animation Frame Fired".
12. Диагностика взаимодействий
На этой тестовой странице отзывчивость очень наглядна, с оценками, таймерами и пользовательским интерфейсом счетчика...но при тестировании средней страницы она более тонкая.
Когда взаимодействия выполняются долго, не всегда понятно, в чем причина. Это может быть:
- Задержка ввода?
- Продолжительность обработки событий
- Задержка вывода ответа на экран
Чтобы измерить отзывчивость страницы, можно использовать Инструменты разработчика. Чтобы выработать привычку, попробуйте следующее:
- Пользуйтесь интернетом как обычно.
- Следите за журналом взаимодействий в режиме просмотра текущих показателей на панели "Производительность" в инструментах разработчика.
- Если вы видите плохо работающее взаимодействие, попробуйте повторить его:
- Если вы не можете повторить проблему, используйте журнал взаимодействий, чтобы получить информацию.
- Если вы можете повторить ее, запишите трассировку на панели "Производительность".
Все задержки
Попробуйте добавить на страницу несколько проблем:
Посмотреть полный код: all_the_things.html
setInterval(() => {
blockFor(1000);
}, 3000);
button.addEventListener('click', () => {
blockFor(1000);
score.incrementAndUpdateUI();
requestAnimationFrame(() => {
blockFor(1000);
});
});
Затем используйте консоль и панель производительности, чтобы диагностировать проблемы.
13. Эксперимент: асинхронная работа
Поскольку вы можете запускать невизуальные эффекты внутри взаимодействий, например делать сетевые запросы, запускать таймеры или просто обновлять глобальное состояние, что происходит, когда они в конечном итоге обновляют страницу?
Как только следующая отрисовка после взаимодействия будет разрешена, даже если браузер решит, что новое обновление отрисовки не требуется, измерение взаимодействия прекратится.
Чтобы проверить это, продолжайте обновлять пользовательский интерфейс из прослушивателя кликов, но запускайте блокирующую работу из тайм-аута.
Посмотреть полный код: timeout_100.html
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
setTimeout(() => {
blockFor(1000);
}, 100);
});
Что будет дальше?
14. Результаты эксперимента с асинхронной работой
Посмотреть полный код: timeout_100.html
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
setTimeout(() => {
blockFor(1000);
}, 100);
});

Теперь взаимодействие происходит быстро, потому что основной поток доступен сразу после обновления интерфейса. Долгая блокирующая задача все равно выполняется, но уже после отрисовки, поэтому пользователь сразу видит отклик интерфейса.
Урок: если вы не можете удалить его, хотя бы переместите его!
Методы
Можно ли улучшить фиксированное значение setTimeout в 100 миллисекунд? Мы по-прежнему хотим, чтобы код выполнялся как можно быстрее, иначе мы бы просто удалили его.
Цель:
- Интерактивный элемент будет запущен
incrementAndUpdateUI(). blockFor()будет выполнена как можно скорее, но не заблокирует следующую отрисовку.- Это позволяет избежать непредсказуемого поведения и "магических тайм-аутов".
Вот несколько способов сделать это:
setTimeout(0)Promise.then()requestAnimationFramerequestIdleCallbackscheduler.postTask()
"requestPostAnimationFrame"
В отличие от requestAnimationFrame (который пытается запуститься до следующей отрисовки и обычно замедляет взаимодействие), requestAnimationFrame + setTimeout создает простой полифил для requestPostAnimationFrame, запуская обратный вызов после следующей отрисовки.
Посмотреть полный код: raf+task.html
function afterNextPaint(callback) {
requestAnimationFrame(() => {
setTimeout(callback, 0);
});
}
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
afterNextPaint(() => {
blockFor(1000);
});
});
Для удобства вы можете даже обернуть его в промис:
Посмотреть полный код: raf+task2.html
async function nextPaint() {
return new Promise(resolve => afterNextPaint(resolve));
}
button.addEventListener('click', async () => {
score.incrementAndUpdateUI();
await nextPaint();
blockFor(1000);
});
15. Несколько взаимодействий (и клики от злости)
Перемещение длительных блокирующих задач может помочь, но эти длительные задачи все равно будут блокировать страницу, влияя на будущие взаимодействия, а также на многие другие анимации и обновления страницы.
Попробуйте снова запустить версию страницы с асинхронной блокировкой (или свою собственную, если вы придумали свой вариант отложенной работы на последнем шаге):
Посмотреть полный код: timeout_100.html
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
setTimeout(() => {
blockFor(1000);
}, 100);
});
Что произойдет, если быстро нажать несколько раз?
Трассировка производительности
Для каждого клика в очередь задач ставится задача длительностью в одну секунду, что гарантирует блокировку основного потока на значительное время.

Когда такие длительные задачи накладываются на новые клики, это приводит к медленному взаимодействию, даже если сам прослушиватель событий возвращает результат почти сразу. Мы создали ту же ситуацию, что и в предыдущем эксперименте с задержками ввода. Только на этот раз задержка ввода вызвана не setInterval, а работой, запущенной более ранними прослушивателями событий.
Стратегии
В идеале длительные задачи нужно полностью устранить.
- Удалите ненужный код, особенно скрипты.
- Оптимизируйте код, чтобы избежать выполнения длительных задач.
- Прерывайте устаревшую работу, когда поступают новые взаимодействия.
16. Стратегия 1. Устранение дребезга
Классическая стратегия. Если взаимодействия происходят быстро и обработка или сетевые эффекты требуют больших затрат, намеренно отложите начало работы, чтобы вы могли отменить и перезапустить ее. Этот шаблон полезен для пользовательских интерфейсов, таких как поля автозаполнения.
- Используйте
setTimeout, чтобы отложить начало ресурсоемкой работы с помощью таймера, например на 500–1000 миллисекунд. - Сохраните идентификатор таймера.
- Если поступает новое взаимодействие, отмените предыдущий таймер с помощью
clearTimeout.
Полный код: debounce.html
let timer;
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
blockFor(1000);
}, 1000);
});
Трассировка производительности

Несмотря на несколько кликов, в итоге выполняется только одна задача blockFor, которая ожидает, пока не пройдет целая секунда без кликов, прежде чем запуститься. Для взаимодействий, которые происходят сериями, например ввода текста или элементов, на которые пользователи нажимают несколько раз подряд, лучше всего использовать стратегию по умолчанию.
17. Стратегия 2. Прерывание длительной работы
Однако есть вероятность, что клик произойдет сразу после окончания периода устранения дребезга, попадет в середину длительной задачи и станет очень медленным взаимодействием из-за задержки ввода.
В идеале, если взаимодействие происходит в середине нашей задачи, мы хотим приостановить нашу работу, чтобы любые новые взаимодействия обрабатывались сразу. Как это сделать?
Существуют такие API, как isInputPending, но обычно лучше разбивать длительные задачи на части.
Много setTimeout
Первая попытка: сделайте что-нибудь простое.
Посмотреть полный код: small_tasks.html
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
requestAnimationFrame(() => {
setTimeout(() => blockFor(100), 0);
setTimeout(() => blockFor(100), 0);
setTimeout(() => blockFor(100), 0);
setTimeout(() => blockFor(100), 0);
setTimeout(() => blockFor(100), 0);
setTimeout(() => blockFor(100), 0);
setTimeout(() => blockFor(100), 0);
setTimeout(() => blockFor(100), 0);
setTimeout(() => blockFor(100), 0);
setTimeout(() => blockFor(100), 0);
});
});
Это позволяет браузеру планировать каждую задачу по отдельности, а вводу можно присвоить более высокий приоритет.

Мы вернулись к пяти секундам работы на пять кликов, но каждая односекундная задача на клик была разбита на десять задач по 100 миллисекунд. В результате, даже если несколько взаимодействий перекрываются с этими задачами, ни одно из них не имеет задержки ввода более 100 миллисекунд! Браузер отдает приоритет прослушивателям входящих событий, а не работе setTimeout, и взаимодействия остаются отзывчивыми.
Эта стратегия особенно хорошо работает при планировании отдельных точек входа, например, если у вас есть набор независимых функций, которые необходимо вызывать во время загрузки приложения. Если просто загрузить скрипты и запустить все во время их оценки, то по умолчанию все может выполняться в рамках одной гигантской длительной задачи.
Однако этот подход не так эффективен для разделения сильно связанного кода, например цикла for, использующего общее состояние.
Теперь с yield()
Однако мы можем использовать современные async и await, чтобы легко добавлять точки приостановки в любую функцию JavaScript.
Пример:
Посмотреть полный код: yieldy.html
// Polyfill for scheduler.yield()
async function schedulerDotYield() {
return new Promise(resolve => {
setTimeout(resolve, 0);
});
}
async function blockInPiecesYieldy(ms) {
const ms_per_part = 10;
const parts = ms / ms_per_part;
for (let i = 0; i < parts; i++) {
await schedulerDotYield();
blockFor(ms_per_part);
}
}
button.addEventListener('click', async () => {
score.incrementAndUpdateUI();
await blockInPiecesYieldy(1000);
});
Как и раньше, основной поток освобождается после выполнения части работы, и браузер может реагировать на любые входящие взаимодействия, но теперь вместо отдельных setTimeout требуется только await schedulerDotYield(), что делает его достаточно эргономичным для использования даже в середине цикла for.
Теперь с AbortContoller()
Это сработало, но каждое взаимодействие планирует больше работы, даже если поступили новые взаимодействия и могли изменить работу, которую необходимо выполнить.
При использовании стратегии устранения дребезга мы отменяли предыдущий тайм-аут при каждом новом взаимодействии. Можно ли сделать что-то подобное здесь? Один из способов сделать это – использовать AbortController().
Посмотреть полный код: aborty.html
// Polyfill for scheduler.yield()
async function schedulerDotYield() {
return new Promise(resolve => {
setTimeout(resolve, 0);
});
}
async function blockInPiecesYieldyAborty(ms, signal) {
const parts = ms / 10;
for (let i = 0; i < parts; i++) {
// If AbortController has been asked to stop, abandon the current loop.
if (signal.aborted) return;
await schedulerDotYield();
blockFor(10);
}
}
let abortController = new AbortController();
button.addEventListener('click', async () => {
score.incrementAndUpdateUI();
abortController.abort();
abortController = new AbortController();
await blockInPiecesYieldyAborty(1000, abortController.signal);
});
Когда происходит клик, запускается цикл blockInPiecesYieldyAborty for, который выполняет все необходимые действия, периодически освобождая основной поток, чтобы браузер мог реагировать на новые взаимодействия.
Когда поступает второй клик, первый цикл помечается как отмененный с помощью AbortController и запускается новый цикл blockInPiecesYieldyAborty. В следующий раз, когда первый цикл должен будет запуститься снова, он заметит, что signal.aborted теперь true, и немедленно вернется, не выполняя дальнейших действий.

18. Заключение
Разбиение всех длительных задач позволяет сайту реагировать на новые взаимодействия. Это позволяет быстро получить первоначальный отзыв, а также принять решения, например, о прерывании выполняемой работы. Иногда это означает планирование точек входа как отдельных задач. Иногда это означает, что нужно добавить точки уступки дороги там, где это удобно.
Внимание!
- INP учитывает все взаимодействия.
- Каждое взаимодействие измеряется от ввода до следующей отрисовки – так, как пользователь видит скорость отклика.
- На скорость отклика влияют задержка ввода, длительность обработки события и задержка отрисовки.
- В инструментах разработчика можно легко измерить INP и разбить взаимодействия по категориям.
Стратегии
- Не используйте на страницах код, который выполняется слишком долго (длительные задачи).
- Переместите ненужный код из прослушивателей событий до следующей отрисовки.
- Убедитесь, что обновление отрисовки эффективно для браузера.