Пошук уроків, статей та іншого контенту
Порівняємо послідовний і паралельний запуск операцій та виберемо стратегію залежно від залежностей між ними.
Коли програма виконує кілька асинхронних операцій, важливо вирішити, у якому порядку їх запускати.
Послідовний запуск — наступна операція починається після завершення попередньої.
Паралельний запуск — незалежні операції запускаються майже одночасно, а програма очікує завершення всіх потрібних операцій.
У Node.js асинхронні операції зазвичай очікують мережеві відповіді, роботу з файлами, базою даних або таймерами. Поки така операція очікує результат, Node.js може обробляти інші задачі.
У контексті асинхронного коду «паралельно» часто означає конкурентний запуск кількох операцій, а не виконання JavaScript-коду на кількох процесорах одночасно.
Послідовний запуск потрібен, коли одна операція залежить від результату іншої.
Наприклад:
Отримати користувача.
За його ідентифікатором отримати замовлення.
Для кожного замовлення отримати деталі.
У такому сценарії неможливо почати наступний крок, поки не відомі дані попереднього.
const delay = (milliseconds) =>
new Promise((resolve) => {
setTimeout(resolve, milliseconds);
});
async function getUser() {
await delay(500);
return {
id: 42,
name: "Olena",
};
}
async function getOrders(userId) {
await delay(700);
return [
{ id: 101, userId, total: 1200 },
{ id: 102, userId, total: 800 },
];
}
async function main() {
const startedAt = Date.now();
// Спочатку потрібно отримати користувача.
const user = await getUser();
// Цей виклик використовує id, отриманий на попередньому кроці.
const orders = await getOrders(user.id);
console.log({
user,
orders,
elapsed: `${Date.now() - startedAt} ms`,
});
}
main().catch((error) => {
console.error("Помилка:", error);
});Загальний час буде приблизно 500 + 700 = 1200 мс, оскільки друга операція починається лише після завершення першої.
Послідовний підхід слід використовувати, якщо:
наступна операція потребує результату попередньої;
операції змінюють один і той самий ресурс;
порядок виконання впливає на правильність даних;
потрібно зупинити процес одразу після помилки на попередньому кроці.
Наприклад, не можна створити запис із userId, якщо користувача ще не створено:
const user = await createUser(userData);
const profile = await createProfile(user.id);Тут createProfile залежить від результату createUser.
Якщо операції не залежать одна від одної, очікувати їх по черзі неефективно.
Припустімо, потрібно завантажити:
профіль користувача;
список повідомлень;
налаштування застосунку.
Якщо ці операції незалежні, їх можна запустити разом за допомогою Promise.all().
const delay = (milliseconds, value) =>
new Promise((resolve) => {
setTimeout(() => resolve(value), milliseconds);
});
async function getProfile() {
return delay(800, {
name: "Olena",
role: "developer",
});
}
async function getMessages() {
return delay(500, [
{ id: 1, text: "Привіт!" },
{ id: 2, text: "Як справи?" },
]);
}
async function getSettings() {
return delay(300, {
theme: "dark",
language: "uk",
});
}
async function main() {
const startedAt = Date.now();
// Усі незалежні операції запускаються одразу.
const [profile, messages, settings] = await Promise.all([
getProfile(),
getMessages(),
getSettings(),
]);
console.log({
profile,
messages,
settings,
elapsed: `${Date.now() - startedAt} ms`,
});
}
main().catch((error) => {
console.error("Помилка:", error);
});Загальний час буде приблизно 800 мс — за тривалістю найдовшої операції, а не сумою всіх тривалостей.
await у циклі може бути повільнимРозглянемо масив ідентифікаторів товарів:
const productIds = [101, 102, 103];Такий код виконує запити послідовно:
const products = [];
for (const productId of productIds) {
const product = await getProduct(productId);
products.push(product);
}Це правильний варіант, якщо запити повинні виконуватися один за одним. Але якщо кожен товар можна завантажувати незалежно, такий код без потреби збільшує час очікування.
Паралельний варіант:
const products = await Promise.all(
productIds.map((productId) => getProduct(productId))
);Тут map() створює масив промісів, а Promise.all() очікує їх усі.
Promise.all()Результати повертаються в тому самому порядку, що й вхідні проміси, навіть якщо завершуються вони в іншому порядку:
const results = await Promise.all([
getProduct(101),
getProduct(102),
getProduct(103),
]);
// results[0] відповідає getProduct(101),
// results[1] відповідає getProduct(102),
// results[2] відповідає getProduct(103).Promise.all()Якщо хоча б один проміс відхилено, Promise.all() також буде відхилено:
async function main() {
try {
const results = await Promise.all([
getProfile(),
getMessages(),
getSettings(),
]);
console.log(results);
} catch (error) {
console.error("Не вдалося отримати всі дані:", error);
}
}Це зручно, коли без будь-якого результату неможливо продовжити роботу.
Важливо: помилка Promise.all() не означає, що інші вже запущені операції автоматично припинилися. Вони можуть продовжити виконання у фоні. Promise.all() лише перестає очікувати їх як єдину групу після першої помилки.
Якщо потрібно отримати результат кожної операції, навіть коли деякі завершилися помилкою, можна використати Promise.allSettled():
const results = await Promise.allSettled([
getProfile(),
getMessages(),
getSettings(),
]);
for (const result of results) {
if (result.status === "fulfilled") {
console.log("Успішний результат:", result.value);
} else {
console.error("Операція завершилася помилкою:", result.reason);
}
}Promise.allSettled() повертає об’єкти зі статусом:
fulfilled — операція успішна;
rejected — операція завершилася помилкою.
Перед запуском операцій поставте кілька запитань:
Чи потрібен результат однієї операції для запуску іншої?
Чи змінюють операції спільний ресурс?
Чи важливий порядок виконання?
Чи можна обробити часткову помилку?
Чи дозволяє зовнішній сервіс стільки одночасних запитів?
Використовуйте:
const first = await firstOperation();
const second = await secondOperation(first);Коли:
є залежність між операціями;
важливий порядок;
паралельний запуск може спричинити конфлікт.
Використовуйте:
const [first, second] = await Promise.all([
firstOperation(),
secondOperation(),
]);Коли:
операції незалежні;
потрібно зменшити загальний час очікування;
результат потрібен лише після завершення всіх операцій.
У реальному коді ці підходи часто поєднуються.
Наприклад:
Спочатку отримати користувача.
Після цього паралельно отримати його замовлення та повідомлення.
async function loadUserDashboard(userId) {
// Цей крок необхідний перед наступними операціями.
const user = await getUser(userId);
// Ці операції незалежні одна від одної.
const [orders, messages] = await Promise.all([
getOrders(user.id),
getMessages(user.id),
]);
return {
user,
orders,
messages,
};
}Так код зберігає необхідні залежності, але не втрачає продуктивність там, де операції можна виконати одночасно.
Погано:
const profile = await getProfile();
const messages = await getMessages();
const settings = await getSettings();Якщо операції незалежні, краще:
const [profile, messages, settings] = await Promise.all([
getProfile(),
getMessages(),
getSettings(),
]);Promise.all() після awaitПогано:
const profile = await getProfile();
const messages = await getMessages();
const result = await Promise.all([profile, messages]);На момент виклику Promise.all() обидві операції вже завершилися, тому паралельного запуску не відбулося.
Правильно:
const profilePromise = getProfile();
const messagesPromise = getMessages();
const [profile, messages] = await Promise.all([
profilePromise,
messagesPromise,
]);Погано:
const [user, orders] = await Promise.all([
getUser(),
getOrders(user.id),
]);У цьому коді user ще не існує в момент створення другого промісу.
Правильно:
const user = await getUser();
const orders = await getOrders(user.id);Паралельний запуск не означає, що потрібно запускати тисячі операцій одночасно. Це може:
перевантажити базу даних;
перевищити ліміти API;
збільшити використання пам’яті;
спричинити відмови через надто велику кількість з’єднань.
Кількість одночасних операцій потрібно узгоджувати з можливостями зовнішнього сервісу.
await у послідовному коді зручний для операцій із залежностями.
Promise.all() дає змогу очікувати незалежні операції одночасно.
Час паралельного виконання приблизно визначається найдовшою операцією.
Promise.all() завершується помилкою, якщо відхилено хоча б один проміс.
Promise.allSettled() дає змогу отримати результат кожної операції незалежно від помилок.
Найкраща стратегія часто комбінує послідовні етапи та паралельні незалежні операції.
Перед паралельним запуском потрібно перевірити залежності, порядок змін і допустиме навантаження.