Почему страница зависает при обработке большого массива в JavaScript
Что проверяют
Понимаете ли вы, что выполнение JavaScript и отрисовка делят один поток, и отличаете ли асинхронность от параллелизма. Заодно проверяют, знаете ли вы способы разгрузить главный поток и их цену.
Как отвечать
В браузере скрипт, пересчёт стилей, компоновка и отрисовка идут в одном потоке. Пока крутится синхронный цикл, управление не возвращается циклу событий: браузер не может ни нарисовать кадр, ни обработать клик. При шестидесяти кадрах в секунду на кадр есть около шестнадцати миллисекунд, и задача длиннее съедает кадры целиком.
Дальше по порядку, от дешёвого к дорогому:
- не делать работу: виртуализация списка вместо отрисовки всех строк,
MapилиSetвместо поиска вложенным циклом, фильтрация и сортировка на стороне сервера; - резать на куски и отдавать управление: обработать часть, вернуть поток браузеру, продолжить в следующем тике. Отдавать надо по бюджету времени, а не по числу элементов;
- выносить в Web Worker: чистое вычисление уезжает в отдельный поток, главный остаётся отзывчивым.
async function run(items, step) {
let start = performance.now();
for (const item of items) {
step(item);
if (performance.now() - start > 5) {
await new Promise((r) => setTimeout(r));
start = performance.now();
}
}
}Где спотыкаются
Оборачивают тот же синхронный цикл в async и считают, что он перестал блокировать. await на готовом промисе переносит код в микрозадачу, а между микрозадачами браузер не рисует: очередь разгребается целиком.
Второе: дробят работу по одному элементу на setTimeout. У вложенных таймеров есть минимальная задержка, и обработка растягивается в разы. Резать надо крупными кусками, по бюджету времени.
Третье: переносят в воркер массив объектов и удивляются, что стало хуже. Передача копирует данные структурным клонированием, и эта сериализация выполняется в главном потоке. Дёшево уезжают типизированные массивы через передачу владения буфером или данные, которые воркер запрашивает сам.