О практической работе
1. Введение
Интерактивная демонстрация и лабораторная работа по изучению взаимодействия со следующей отрисовкой (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 и Таймер не являются обязательными для измерения взаимодействия. Они добавлены лишь для того, чтобы немного упростить визуализацию отклика.
Попробуйте это
Попробуйте взаимодействовать с кнопкой «Увеличить» и наблюдайте, как растёт счёт. Изменяются ли значения INP и Interaction с каждым шагом?
INP измеряет, сколько времени проходит с момента взаимодействия пользователя до того момента, когда страница фактически отображает отрисованное обновление для пользователя.
3. Измерение взаимодействий с помощью Chrome DevTools
Откройте DevTools из меню Дополнительные инструменты > Инструменты разработчика , щелкнув правой кнопкой мыши по странице и выбрав Проверить , или воспользовавшись сочетанием клавиш .
Переключитесь на панель «Производительность» , которую вы будете использовать для измерения взаимодействий.
Затем зафиксируйте взаимодействие на панели «Производительность».
- Пресс-релиз.
- Взаимодействуйте со страницей (нажмите кнопку « Увеличить »).
- Остановите запись.
На открывшейся временной шкале вы увидите дорожку «Взаимодействия» . Разверните её, нажав на треугольник слева.
Появятся два взаимодействия. Увеличьте масштаб второго, прокрутив страницу или удерживая клавишу W.
Наведя курсор на взаимодействие, вы увидите, что взаимодействие было быстрым, без затрат времени на обработку и с минимальным количеством времени на задержку ввода и задержку представления , точная продолжительность которых будет зависеть от скорости вашего компьютера.
4. Долгосрочные прослушиватели событий
Откройте файл index.js
и раскомментируйте функцию blockFor
внутри прослушивателя событий.
Смотреть полный код: click_block.html
button.addEventListener('click', () => {
blockFor(1000);
score.incrementAndUpdateUI();
});
Сохраните файл. Сервер увидит изменения и обновит страницу.
Попробуйте снова взаимодействовать со страницей. Теперь взаимодействие будет заметно медленнее.
Трассировка производительности
Сделайте еще одну запись на панели «Performance», чтобы увидеть, как это выглядит.
То, что раньше было коротким взаимодействием, теперь занимает целую секунду.
При наведении курсора на взаимодействие обратите внимание, что почти всё время уходит на «Длительность обработки» — время, необходимое для выполнения обратных вызовов прослушивателя событий. Поскольку блокирующий вызов 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 для усиления эффектов GPU
- Добавление очень больших изображений высокого разрешения
- Использование 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.
- Если вы видите неэффективное взаимодействие, попробуйте повторить его:
- Если вы не можете повторить это, воспользуйтесь журналом взаимодействия, чтобы получить информацию.
- Если вы можете повторить это, запишите трассировку на панели «Производительность».
Все задержки
Попробуйте добавить на страницу немного всех этих проблем:
Смотреть полный код: 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()
-
requestAnimationFrame
-
requestIdleCallback
-
scheduler.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);
});
Как и прежде, основной поток завершается после выполнения части работы, и браузер может реагировать на любые входящие взаимодействия, но теперь все, что требуется, — это 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);
});
При щелчке запускается цикл blockInPiecesYieldyAborty
for
выполняет всю необходимую работу, периодически возвращая основной поток, чтобы браузер оставался восприимчивым к новым взаимодействиям.
При поступлении второго щелчка первый цикл помечается как отмененный с помощью AbortController
и запускается новый цикл blockInPiecesYieldyAborty
— в следующий раз, когда первый цикл будет запланирован для повторного запуска, он заметит, что signal.aborted
теперь имеет true
, и немедленно вернется без выполнения дальнейших действий.
18. Заключение
Разбиение всех длительных задач позволяет сайту быстрее реагировать на новые взаимодействия. Это позволяет быстро предоставлять первоначальную обратную связь и принимать решения, например, о приостановке текущей работы. Иногда это означает планирование точек входа как отдельных задач. Иногда это означает добавление точек «выхода» там, где это удобно.
Помнить
- INP измеряет все взаимодействия.
- Каждое взаимодействие измеряется от ввода до следующей отрисовки — так пользователь видит отзывчивость.
- Задержка ввода, длительность обработки событий и задержка представления влияют на скорость реагирования взаимодействия.
- С помощью DevTools вы можете легко измерить INP и сбои взаимодействия!
Стратегии
- Не размещайте на своих страницах долго выполняемый код (длительные задачи).
- Переместите ненужный код из прослушивателей событий до момента следующей отрисовки.
- Убедитесь, что обновление рендеринга эффективно для браузера.