Что делать чтобы не блокировать основной поток при обработке большого массива без использования web workers
Что проверяют
Понимаете ли вы, что «асинхронно» и «не блокирует поток» это разные вещи. Промис не создаёт параллелизма: тяжёлый цикл в колбэке занимает поток ровно так же. Ждут, что вы разрежете работу на куски и научитесь уступать браузеру время на кадр.
Как отвечать
Скажите сразу: без воркеров вычисления всё равно идут в основном потоке, поэтому задача не ускорить их, а не дать им держать поток дольше кадра. Схема стандартная:
- резать работу по времени, а не по числу элементов: считать, пока не вышли за несколько миллисекунд, потом уступить;
- уступать так, чтобы браузер успел отрисовать кадр. Микрозадача (
await Promise.resolve()) не годится: очередь микрозадач разгребается до рендера. ГодятсяsetTimeout,MessageChannel,scheduler.yield()там, где он доступен; - фоновую работу, результат которой не нужен прямо сейчас, отдавать в
requestIdleCallback; requestAnimationFrameбрать только для того, что связано с отрисовкой кадра.
async function run(items, step) {
let t = performance.now();
for (const it of items) {
step(it);
if (performance.now() - t > 5) {
await new Promise((r) => setTimeout(r));
t = performance.now();
}
}
}Сильный ответ добавляет, как сделать работу меньше: виртуализация списка, индекс вместо перебора, расчёт только видимой части.
Где спотыкаются
Обрабатывают по одному элементу за кадр через requestAnimationFrame. При шестидесяти кадрах в секунду сто тысяч элементов растянутся почти на полчаса, зато интерфейс формально «отзывчивый».
Второе: оборачивают каждый элемент в промис с setTimeout и называют это параллельной обработкой. Параллелизма нет, а задержка добавляется на каждый элемент: вложенные таймеры браузер ограничивает снизу.
Третье: заводят все таймеры разом в одном цикле. Планировщик получает тысячи задач, отменить их нечем, и новая порция данных встаёт в очередь за старой.