async/await vs Promise.all: как не тормозить при загрузке данных
-
Если ты загружаешь список пользователей и делаешь три отдельных await подряд - каждый ждёт завершения предыдущего. Результат: три запроса выполняются друг за другом, хотя могли бы стартовать одновременно. Promise.all решает эту проблему, запуская всё параллельно и экономя время.
Это не просто теория. На практике разница может быть 3-5 секунд против 1 секунды. Для user experience это критично, для батареи мобильного - тоже.
Где настоящая беда: последовательный await
Представь такой код: ты запрашиваешь данные пользователя, потом его посты, потом комментарии. Каждый запрос - это отдельный await, и они выстраиваются в очередь.
async function fetchUserData(userId) { const user = await fetch(`/api/users/${userId}`); const posts = await fetch(`/api/posts/${userId}`); const comments = await fetch(`/api/comments/${userId}`); return { user, posts, comments }; }Это работает как на одной плите готовить три блюда по очереди. Сначала борщ (2 сек), потом салат (1 сек), потом компот (0.5 сек). Итого - 3.5 секунды. Но ведь все три можно готовить одновременно на разных конфорках!
Проблема в том, что каждый await останавливает выполнение функции. Последующий код не выполняется, пока текущий промис не разрешится. Это вполне нормально, если данные зависят друг от друга (например, сначала загрузи пользователя, потом его роль, потом права доступа). Но если операции независимы - это чистая потеря времени.
Вот почему такой подход опасен для production:
- Ненужные задержки: три параллельных сетевых операции могут занять одну, максимум 1.5 секунды вместо 3.5.
- Плохой UX: пользователь смотрит на спиннер дольше, чем нужно.
- Нагрузка на сервер: если у тебя тысяча юзеров делает такое одновременно, серверу придётся держать больше соединений дольше.
- Батарея: мобильные устройства расходуют энергию не только на передачу данных, но и на ожидание результатов.
Promise.all: как готовить всё сразу
Вместо того чтобы ждать один результат, потом запускать следующий запрос, ты запускаешь все одновременно. Promise.all берёт массив промисов, инициирует их все и ждёт, пока все они разрешатся.
async function fetchUserData(userId) { const [user, posts, comments] = await Promise.all([ fetch(`/api/users/${userId}`), fetch(`/api/posts/${userId}`), fetch(`/api/comments/${userId}`) ]); return { user, posts, comments }; }Разница огромная. Все три fetch стартуют мгновенно. Они работают параллельно на уровне сетевого стека (это не истинная многопоточность, но для операций ввода-вывода - результат идентичен). Функция ждёт только самый медленный запрос. Если каждый занимает примерно секунду, итого - всё равно около секунды, а не трёх.
Промис возвращает результаты в том же порядке, в котором ты передал промисы. Это удобно - не нужно отслеживать, какой результат откуда взялся.
Основные преимущества:
- Скорость: все независимые операции выполняются одновременно.
- Простота обработки ошибок: один try-catch ловит ошибки из всех промисов в массиве.
- Читаемость: видно сразу, что это параллельные операции, а не последовательные.
- Меньше нагрузки: ты не держишь соединение активным дольше, чем нужно.
Когда async/await последовательный - это правильно
У последовательного подхода есть свой смысл. Если второй запрос зависит от результата первого, ты просто обязан ждать.
async function fetchUserWithRole(userId) { const userResponse = await fetch(`/api/users/${userId}`); const user = await userResponse.json(); const roleResponse = await fetch(`/api/roles/${user.roleId}`); const role = await roleResponse.json(); return { user, role }; }Здесь второй запрос нужен roleId из первого ответа. Иначе это не сработает. И в этом случае последовательность - не недостаток, а требование логики.
Ещё несколько сценариев, где последовательность оправдана:
- Валидация перед следующим шагом: сначала проверил данные, потом сохранил их.
- Зависимые операции: загрузил конфиг, потом инициализировал модули на его основе.
- Критичное порядок: например, начисление баланса после проверки прав доступа.
- Обработка ошибок: если первый запрос упал, нет смысла делать второй.
Гибридный подход: микс параллели и последовательности
В реальной жизни часто нужно что-то комбинировать. Например: загрузи пользователя и список его друзей параллельно, потом загрузи данные друзей.
async function fetchUserWithFriends(userId) { // Шаг 1: параллельно const [userResponse, friendsResponse] = await Promise.all([ fetch(`/api/users/${userId}`), fetch(`/api/friends/${userId}`) ]); const user = await userResponse.json(); const friends = await friendsResponse.json(); // Шаг 2: параллельно (зависит от результата шага 1) const friendDetails = await Promise.all( friends.map(friend => fetch(`/api/users/${friend.id}`).then(r => r.json())) ); return { user, friends, friendDetails }; }Этот паттерн часто встречается в реальных приложениях:
- Загрузи основные данные и справочники параллельно.
- Потом, когда есть результаты, используй их для следующей партии параллельных операций.
- Это даёт лучшее из обоих миров: скорость и логическая стройность.
Ошибки и граничные случаи
Примерно половина багов с Promise.all происходит из-за невнимательности. Вот типичные ловушки:
Проблема 1: одна ошибка рушит всё
Если хотя бы один промис из массива rejected, Promise.all вернёт ошибку. Остальные промисы могут остаться в памяти незаконченными. Часто это нормально, но иногда нужно, чтобы все операции выполнились, даже если какие-то упали.
// Плохо: если один fetch упадёт, остальные будут заморожены await Promise.all([ fetch('/api/data1').then(r => r.json()), fetch('/api/data2').then(r => r.json()), fetch('/api/data3').then(r => r.json()) ]);Для этого есть Promise.allSettled - он ждёт все промисы, независимо от результата:
const results = await Promise.allSettled([ fetch('/api/data1').then(r => r.json()), fetch('/api/data2').then(r => r.json()), fetch('/api/data3').then(r => r.json()) ]); // results содержит {status, value} или {status, reason} для каждогоПроблема 2: забыли обернуть в Promise
// Баг: это не асинхронная функция, она вернёт undefined const fetchUser = userId => { fetch(`/api/users/${userId}`) }; await Promise.all([fetchUser(1), fetchUser(2)]);Практически невозможно уловить без линтера.
Проблема 3: утечки памяти от незавершённых промисов
Если ты перепутал и запустил Promise.all, но никогда не awaited результат, промисы могут остаться висеть в памяти. Это редко, но случается при неправильной обработке ошибок.
Типичные случаи, где нужна осторожность:
- Массивы, которые могут быть пустыми (Promise.all([]) резолвится в пустой массив - это okay).
- Промисы, которые никогда не резолвятся (например, подписки на события - используй abortController).
- Вложенные Promise.all (работают, но код становится сложнее читать).
- Смешивание синхронного и асинхронного кода в одном массиве (Promise.all это переносит, но лучше явно оборачивать).
Сравнение в цифрах
Сценарий Последовательный await Promise.all Разница 3 запроса по 1 сек 3 сек 1 сек 3x быстрее 10 запросов по 0.5 сек 5 сек 0.5 сек 10x быстрее 1 запрос 2 сек + 2 по 0.5 сек (с зависимостью) 3 сек 2.5 сек 1.2x быстрее Зависимые операции (требуют последовательность) необходимо нет смысла N/A Цифры условные, но суть понятна. С большим количеством независимых операций выигрыш может быть в разы. И это не просто теория - это видно в профайлере DevTools.
На что не стоит забивать голову
Часто разработчики переживают: “Может ли Promise.all создать проблемы с производительностью?” На самом деле - нет, если ты не запускаешь тысячи запросов одновременно. Браузер и Node.js нормально справляются с 20-50 параллельными сетевыми операциями. Если тебе нужно больше, это уже архитектурный вопрос - может, нужен бекенд, который агрегирует данные вместо того, чтобы фронтенд дёргал API по одному.
Ещё один момент: Promise.all работает не с настоящей многопоточностью. JavaScript по-прежнему однопоточный. Но для операций ввода-вывода (сетевые запросы, файловые операции, работа с БД) это не важно - операционная система и браузер справляются с параллелизмом на своём уровне. Promise.all просто позволяет тебе не блокировать JavaScript-поток, пока операции выполняются.
Здравствуйте! Похоже, вас заинтересовала эта беседа, но у вас ещё нет аккаунта.
Надоело каждый раз пролистывать одни и те же посты? Зарегистрировав аккаунт, вы всегда будете возвращаться на ту же страницу, где были раньше, и сможете выбирать, получать ли уведомления о новых ответах (по электронной почте или в виде push-уведомлений). Вы также сможете сохранять закладки и ставить лайки постам, чтобы выразить свою благодарность другим участникам сообщества.
С вашими комментариями этот пост мог бы стать ещё лучше 💗
Зарегистрироваться Войти© 2024 - 2026 ExLends, Inc. Все права защищены.