Вопрос про порядок console.log полезен, когда за ним стоит реальная проблема: почему кнопка не отвечает, spinner не успевает появиться или callback таймера выполняется позже ожидаемого. Заученная формула «сначала макро, потом микро» часто мешает, потому что не объясняет синхронную часть Promise и точки, в которых среда обрабатывает очереди.
Начните с одного браузерного примера
Запустите код в отдельном скрипте страницы или консоли браузера. Здесь нет process.nextTick, модульного импорта и сетевых запросов. Так мы отделяем базовую механику от особенностей среды. Перед запуском напишите порядок вывода на бумаге и для каждой строки объясните, когда именно она попала в очередь.
JavaScript// Браузерный пример: предскажите порядок
console.log('start')
setTimeout(() => console.log('timer'), 0)
Promise.resolve().then(() => console.log('promise'))
queueMicrotask(() => console.log('microtask'))
new Promise(resolve => {
console.log('executor')
resolve('done')
}).then(value => console.log(value))
console.log('end')
Для этого примера порядок: start, executor, end, promise, microtask, done, timer. Сначала выполняется синхронный скрипт, включая функцию-исполнитель new Promise. Обработчики then и queueMicrotask зарегистрированы в таком порядке. Они выполняются на микрозадачном checkpoint до следующей задачи таймера. Сам Promise не является отдельным потоком исполнения.
Что меняется, когда callback добавляет ещё работу
Микрозадача может поставить новую микрозадачу. Обработка очереди не обязана закончиться после того набора, который существовал в начале checkpoint. Именно поэтому длинная цепочка then может откладывать передачу управления браузеру. В следующем примере вложенная микрозадача зарегистрирована из first, когда second уже находится в очереди.
JavaScript// Вложенная регистрация не обгоняет готовую очередь
queueMicrotask(() => {
console.log('first')
queueMicrotask(() => console.log('nested'))
})
queueMicrotask(() => console.log('second'))
Результат - first, second, nested. Это маленькая проверка понимания: если вы ожидали first, nested, second, вы представили микрозадачу как немедленный вложенный вызов. Очередь и стек вызовов - разные механизмы. При этом порядок вывода не говорит, сколько времени заняло выполнение каждой задачи.
Почему Promise не спасает тяжёлый обработчик
Оборачивание сортировки большого массива в Promise.resolve().then не переносит сортировку в другой поток. Оно меняет момент запуска, но оставляет вычисление на основном потоке. Когда пользователь нажимает кнопку, интерфейс может долго ждать обработчика, а затем ещё ждать вычисления layout и paint. Внешне асинхронная функция поэтому не обязательно даёт отзывчивый интерфейс.
Другое распространённое решение - поставить spinner в state, затем сразу выполнить тяжёлый расчёт. Изменённое состояние не гарантирует, что браузер уже нарисовал spinner: ему ещё нужно получить время на рендеринг. Вместо добавления случайной задержки сначала измерьте длительность работы. Возможно, достаточно сократить объём данных, заранее построить индекс или разделить обработку на части.
Практический выбор
- Небольшие независимые части можно обрабатывать порциями, возвращая управление среде между ними. Способ планирования выбирайте с учётом поддерживаемых браузеров.
- Тяжёлые независимые вычисления подходят для Worker, но передача больших объектов тоже имеет цену.
- Для DOM-зависимой работы сначала разделите чтение геометрии и запись стилей: постоянное чередование может провоцировать лишние layout.
- Не назначайте произвольный таймер на 100 мс как доказательство, что кадр уже отрисован. Проверьте цель изменения в профиле производительности.
Оговорка для Node.js и собеседования
В Node.js есть собственные фазы event loop и очередь process.nextTick. Контекст ESM отличается от синхронного CommonJS: верхний уровень модуля может выполняться в другом контексте обработки микрозадач. Также порядок setImmediate и таймера зависит от места регистрации. Если задача не уточняет среду, спросите о ней перед ответом. Это признак аккуратного рассуждения, а не уход от вопроса.
Как закрепить понимание
- Добавьте ещё один then и предскажите место его вывода.
- Поставьте длительный синхронный цикл перед end и наблюдайте, как задерживаются все callbacks.
- В отдельном приложении замерьте медленный поиск по списку. Свяжите профиль с INP и отзывчивостью.
- Сформулируйте один короткий вывод: что будет параллельным, что просто отложенным и где браузер может отрисовать следующий кадр.
Для подготовки к собеседованиям полезнее уметь построить такой разбор, чем хранить десятки последовательностей в памяти. В практикуме асинхронность связана с запросами, ошибками и состояниями реального интерфейса.
Документация
Поведение Promise: MDN Promise. Отличия среды исполнения: Node.js Event Loop. Браузерные примеры статьи не содержат Node-специфичного nextTick.