Как избежать зависания интерфейса при обработке большого массива на клиенте с использованием event loop
Что проверяют
Понимаете ли вы, почему интерфейс вообще зависает: пока задача занимает главный поток, браузер не отрисовывает кадр и не обрабатывает клики. Следом обычно просят объяснить разницу между макрозадачами и микрозадачами и спрашивают, почему дробление через Promise.resolve().then не помогает.
Как отвечать
Начните с механики: event loop берёт одну макрозадачу, затем выполняет всю очередь микрозадач, и только после этого браузер может отрисовать кадр. Долгой задачей считается всё, что дольше 50 мс: она блокирует и отрисовку, и ввод. Дальше варианты по нарастанию:
- дробление на порции: обрабатываете кусок и отдаёте управление через
setTimeout(fn, 0)илиMessageChannel. Порцию лучше мерить временем черезperformance.now()(например, не больше 10 мс работы), а не фиксированным числом элементов; scheduler.yield(): современный способ уступить поток так, чтобы продолжение не встало в конец очереди. Поддерживается не во всех браузерах, нужен запасной вариант черезsetTimeout;requestIdleCallback: для фоновой работы, которая может подождать. Для срочного результата не годится, колбэк может не прийти долго;- Web Worker: настоящий отдельный поток. Данные уходят через
postMessageструктурным клонированием, большиеArrayBufferлучше передавать как transferable, без копирования. DOM из воркера недоступен; - не обрабатывать лишнего: виртуализация списка (
react-window,@tanstack/react-virtual), пагинация, агрегация на сервере.
Хороший финал: как вы это измеряете. Вкладка Performance в DevTools подсвечивает long tasks, а метрика INP показывает, насколько быстро страница реагирует на ввод.
Где спотыкаются
Дробят через Promise или queueMicrotask. Микрозадачи выполняются до отрисовки, поэтому интерфейс висит так же, как и без дробления.
Считают, что async/await сам по себе делает код неблокирующим. Синхронный цикл внутри async-функции блокирует поток точно так же.
Отправляют в воркер огромный массив объектов и удивляются задержке: клонирование само занимает главный поток. Для тяжёлых данных нужны типизированные массивы и передача владения буфером.