1. Введение
Интерактивная демонстрация и лабораторная работа по изучению взаимодействия с 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/ в своем браузере.
Обзор приложения
В верхней части страницы расположен счетчик очков и кнопка «Приращение» . Классическая демонстрация реактивности и отзывчивости!
Под кнопкой расположены четыре измерения:
- INP: текущий показатель INP, который обычно является наихудшим взаимодействием.
- Взаимодействие: оценка самого последнего взаимодействия.
- FPS: количество кадров в секунду основного потока на странице.
- Таймер: анимация работающего таймера, помогающая визуализировать задержку.
Записи FPS и Timer совершенно не нужны для измерения взаимодействий. Они добавлены только для того, чтобы упростить визуализацию реакции.
Попробуйте это
Попробуйте взаимодействовать с кнопкой «Приращение» и наблюдайте, как увеличивается счет. Изменяются ли значения INP и Interaction с каждым приращением?
INP измеряет, сколько времени проходит с момента взаимодействия пользователя до момента, когда страница фактически показывает пользователю отображаемое обновление.
3. Измерение взаимодействия с Chrome DevTools
Откройте DevTools из меню «Дополнительные инструменты» > «Инструменты разработчика» , щелкнув правой кнопкой мыши на странице и выбрав «Проверить» , или воспользовавшись сочетанием клавиш .
Переключитесь на панель «Производительность» , которую вы будете использовать для измерения взаимодействий.
Затем зафиксируйте взаимодействие на панели «Производительность».
- Нажмите запись.
- Взаимодействуйте со страницей (нажмите кнопку «Инкремент» ).
- Остановите запись.
На полученной временной шкале вы найдете дорожку «Взаимодействия» . Разверните его, нажав на треугольник слева.
Появляются два взаимодействия. Увеличьте второе изображение, прокрутив или удерживая клавишу 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
Давайте смоделируем длительную задержку презентации, используя API requestAnimationFrame()
.
Переместите вызов 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
продолжает блокировать следующую отрисовку в течение целой секунды.
Однако обратите внимание на две вещи:
- При наведении вы увидите, что все время взаимодействия теперь тратится на «задержку представления», поскольку блокировка основного потока происходит после возвращения прослушивателя событий.
- Корнем активности основного потока больше не является событие щелчка, а «Вызов кадра анимации».
12. Диагностика взаимодействий
На этой тестовой странице отзывчивость очень наглядна, с оценками, таймерами и пользовательским интерфейсом счетчика... но при тестировании средней страницы она более тонкая.
Когда взаимодействие длится долго, не всегда ясно, кто виноват. Это:
- Задержка ввода?
- Продолжительность обработки события?
- Задержка презентации?
На любой странице вы можете использовать DevTools, чтобы измерить скорость реагирования. Чтобы войти в привычку, попробуйте следующий алгоритм:
- Перемещайтесь по сети, как обычно.
- Необязательно: оставьте консоль DevTools открытой, пока расширение Web Vitals записывает взаимодействия.
- Если вы видите неэффективное взаимодействие, попробуйте повторить его:
- Если вы не можете повторить это, используйте журналы консоли, чтобы получить информацию.
- Если сможете повторить, запишите в панели выступлений.
Все задержки
Попробуйте добавить на страницу понемногу все эти проблемы:
См. полный код: 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 миллисекунд? Вероятно, мы по-прежнему хотим, чтобы код выполнялся как можно быстрее, иначе нам следовало бы просто удалить его!
Цель:
- Взаимодействие будет запускатьсяcrecrementAndUpdateUI
incrementAndUpdateUI()
. -
blockFor()
запустится как можно скорее, но не заблокирует следующую отрисовку. - Это приводит к предсказуемому поведению без «магических тайм-аутов».
Некоторые способы добиться этого включают в себя:
-
setTimeout(0)
-
Promise.then()
-
requestAnimationFrame
-
requestIdleCallback
-
scheduler.postTask()
"запросPostAnimationFrame"
В отличие от самого 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
s
Первая попытка: сделать что-нибудь простое.
См. полный код: 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);
});
Как и прежде, основной поток передается после некоторой части работы, и браузер может реагировать на любые входящие взаимодействия, но теперь все, что требуется, — это await schedulerDotYield()
вместо отдельных setTimeout
, что делает его достаточно эргономичным, чтобы его можно было использовать даже в середина цикла 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);
});
Когда происходит щелчок, он запускает цикл for
blockInPiecesYieldyAborty
, выполняющий всю необходимую работу, периодически возвращая основной поток, чтобы браузер продолжал реагировать на новые взаимодействия.
Когда происходит второй щелчок, первый цикл помечается как отмененный с помощью AbortController
, и запускается новый цикл blockInPiecesYieldyAborty
— в следующий раз, когда запланировано повторное выполнение первого цикла, он замечает, что signal.aborted
теперь имеет true
, и немедленно возвращается без делаем дальнейшую работу.
18. Заключение
Разбивка всех длинных задач позволяет сайту реагировать на новые взаимодействия. Это позволяет быстро предоставить первоначальную обратную связь, а также принять такие решения, как прекращение текущей работы. Иногда это означает планирование точек входа как отдельных задач. Иногда это означает добавление очков «доходности» там, где это удобно.
Помнить
- INP измеряет все взаимодействия.
- Каждое взаимодействие измеряется от ввода до следующей отрисовки — то, как пользователь видит реакцию.
- Задержка ввода, продолжительность обработки событий и задержка представления влияют на скорость реагирования на взаимодействие.
- Вы можете легко измерить INP и нарушения взаимодействия с помощью DevTools!
Стратегии
- Не размещайте на своих страницах долго выполняющийся код (длинные задачи).
- Удалите ненужный код из прослушивателей событий до следующей отрисовки.
- Убедитесь, что само обновление рендеринга эффективно для браузера.