Почему интерфейс не блокируется при выполнении тяжеловесного кода, разбитого на промисы
Что проверяют
Понимаете ли вы, что промис это не поток. Вопрос с подвохом: верный ответ начинается со слов «вообще-то он блокируется», и интервьюер смотрит, поймаете ли вы подмену.
Как отвечать
Скажите прямо: оборачивание тяжёлых вычислений в промисы интерфейс само по себе не спасает. Колбэк промиса это микрозадача, она выполняется на том же единственном потоке, и вся очередь микрозадач разгребается до конца прежде, чем браузер получит шанс обработать клик или нарисовать кадр. Цепочка из сотни then с тяжёлым телом заблокирует страницу ровно так же, как один длинный цикл.
Отзывчивость появляется в трёх случаях:
- работа на самом деле не наша: сеть,
fetch, декодирование картинки, таймер. Поток в это время свободен, промис лишь сообщает о результате; - куски разделены макрозадачей, а не микрозадачей. Тогда между ними вклиниваются ввод и отрисовка:
setTimeout,MessageChannel,requestAnimationFrame, а где доступен,scheduler.yield(); - вычисление вынесено в Web Worker, то есть в другой поток, и главный занят только обменом сообщениями.
Хорошо звучит и метрика: отзывчивость меряют по INP и по длинным задачам в панели производительности. Цель не «использовать промисы», а не занимать поток длинными кусками подряд.
Где спотыкаются
Считают, что await уступает управление браузеру. Он уступает его очереди микрозадач: если промис уже выполнен, продолжение запустится в том же кадре, до отрисовки.
Второе: дробят через setTimeout(fn, 0) и удивляются, что стало медленнее. Вложенные таймеры браузер прижимает к минимальной задержке в несколько миллисекунд, и на тысяче кусков набегают заметные паузы; для чистого дробления лучше подходит MessageChannel.
Третье: уносят в воркер то, что трогает разметку. В воркере нет ни document, ни доступа к DOM, туда выносят чистый расчёт, а данные передают через postMessage, не забывая про цену копирования.