Что происходит при рекурсивном вызове с созданием промисов в JavaScript: зависнет ли страница или будет пропускаться рендер
Что проверяют
Различаете ли вы «не переполняет стек» и «не блокирует кадр». Вопрос с подвохом: асинхронный код тоже умеет намертво повесить вкладку, и от того, через какую очередь крутится рекурсия, зависит, увидит пользователь зависание или плавную работу.
Как отвечать
Разбирать надо три разных сценария.
Синхронная рекурсия без выхода упирается в стек вызовов и падает с ошибкой про превышение его размера. До этого момента вкладка не отвечает, но заканчивается всё быстро и заметно.
Рекурсия через микрозадачи растёт иначе. Каждый шаг стартует с пустого стека, поэтому переполнения не будет вовсе, зато очередь микрозадач разгребается до конца перед отрисовкой. Значит, рендер и обработка ввода не получат управления никогда: вкладка виснет насмерть, без ошибки в консоли.
function loop() { Promise.resolve().then(loop); }
loop(); // вкладка перестанет отвечать, ошибки не будетРекурсия через макрозадачу (setTimeout) ведёт себя прилично: между шагами браузер успевает отрисовать кадр и ответить на клик. Плата за это скорость, у вложенных таймеров есть минимальная задержка в несколько миллисекунд.
Практический вывод: тяжёлую работу либо режут на порции с возвратом управления через макрозадачу, либо выносят в Web Worker, где она вообще не трогает главный поток.
Где спотыкаются
Читают слово «асинхронный» как «не блокирует». Промис не уводит вычисление в другой поток: колбэк выполнится в том же главном потоке, просто позже. Тяжёлый счёт внутри then блокирует страницу ровно так же, как и снаружи.
Уверены, что рекурсия на промисах даёт переполнение стека, и ищут в консоли ошибку. Её нет, и это как раз худший вариант: симптом выглядит как «браузер завис», а причина не видна ни в одном сообщении.
Переписывают то же самое на async/await и считают, что стало лучше. Цикл с await на уже готовых значениях это те же микрозадачи, управление рендеру он не отдаёт. Помогает только выход в макрозадачу или другой поток.