Паралельне та послідовне отримання даних у Next.js
Паралельне отримання даних запускає всі запити одразу і чекає найповільнішого; послідовне чекає завершення кожного запиту перед тим, як почати наступний.
Теорія
TL;DR
- Послідовно = waterfall: 200ms + 300ms + 200ms = 700ms разом
- Паралельно =
Promise.all(): max(200ms, 300ms, 200ms) = 300ms разом - Паралельно - коли запити незалежні; послідовно - коли пізніший запит потребує результат попереднього
Promise.all()завершується з помилкою якщо будь-який запит падає; для часткових збоїв використовуйPromise.allSettled()- Server Components у Next.js не паралелізують запити автоматично - структуру треба прописувати самостійно
Швидкий приклад
// Послідовно: усі затримки складаються
const user = await getUser(id); // 200ms
const posts = await getPosts(id); // 300ms (чекає завершення user)
const comments = await getComments(id); // 200ms (чекає завершення posts)
// Разом: ~700ms
// Паралельно: важливий лише найповільніший запит
const [user, posts, comments] = await Promise.all([
getUser(id), // 200ms
getPosts(id), // 300ms
getComments(id), // 200ms - всі три стартують одночасно
]);
// Разом: ~300msПослідовний підхід додає кожну затримку. Паралельний бере тільки найдовшу. На сторінці з 5-6 незалежними запитами різниця дуже відчутна для користувача.
Головна різниця
Послідовне отримання даних утворює ланцюжок залежностей: кожен await блокує весь render pipeline до завершення запиту. Паралельний підхід створює всі Promise одночасно і чекає завершення останнього. Різниця в часі накопичується: три запити по 300ms займають 900ms послідовно, але тільки 300ms паралельно. Мережева затримка (latency) зазвичай і є вузьким місцем при server-side rendering.
Коли що використовувати
- Паралельно коли запити незалежні (профіль, пости, підписники - жоден не потребує даних іншого)
- Послідовно коли пізніший запит потребує результат попереднього (спочатку отримуємо user, потім команду через
user.teamId) - Змішано коли частина запитів незалежна, а частина ні (паралелізуємо все що має спільний ID, потім послідовно отримуємо залежні дані)
- Streaming з Suspense коли хочемо показати частину UI поки повільніші дані вантажаться у фоні
Порівняльна таблиця
| Патерн | Загальний час | Залежності | Типовий сценарій |
|---|---|---|---|
| Послідовний | Сума всіх запитів | Кожен залежить від попереднього | Ієрархічні дані: user → posts → comments |
Паралельний (Promise.all) | Максимум з усіх | Відсутні | Незалежні дані: профіль, аналітика, сповіщення |
| Змішаний | Оптимізований | Часткові | Більшість реальних застосунків |
| Streaming (Suspense) | Прогресивний | Гнучкі | Показувати UI поступово |
Як це працює всередині
Коли викликаєш Promise.all([...]), JavaScript створює всі Promise одразу - вони починають виконуватись негайно. Потім async-функція призупиняється й чекає завершення останнього. Event loop при цьому вільний для іншої роботи. У Next.js Server Components весь цей процес відбувається під час серверного рендерингу, а клієнт отримує вже готовий HTML.
Я помічаю таку закономірність: розробники додають Promise.all() і вважають що зробили все правильно. Але якщо перед викликом Promise.all() є хоча б один await, це вже послідовне вузьке місце. Код виглядає паралельним, але не є ним.
Типові помилки
Помилка 1: Прихований waterfall всередині нібито паралельного коду
// Неправильно: reviews все одно чекає на product
const product = await getProduct(productId);
const [reviews, recommendations] = await Promise.all([
getReviews(product.id), // все одно чекає product
getRecommendations(productId)
]);
// Правильно: запускаємо всі Promise до будь-якого await
const productPromise = getProduct(productId);
const reviewsPromise = getReviews(productId);
const recommendationsPromise = getRecommendations(productId);
const [product, reviews, recommendations] = await Promise.all([
productPromise,
reviewsPromise,
recommendationsPromise
]);Помилка 2: Promise.all() падає при будь-якій помилці
// Неправильно: якщо getComments впаде, втрачаємо user і posts теж
const [user, posts, comments] = await Promise.all([
getUser(id),
getPosts(id),
getComments(id) // одна помилка ламає все
]);
// Правильно: allSettled() для некритичних даних
const results = await Promise.allSettled([
getUser(id),
getPosts(id),
getComments(id)
]);
const user = results[0].status === 'fulfilled' ? results[0].value : null;
const posts = results[1].status === 'fulfilled' ? results[1].value : [];
const comments = results[2].status === 'fulfilled' ? results[2].value : [];Помилка 3: Послідовне отримання в циклах (проблема N+1)
// Неправильно: по одному користувачу за раз
const userIds = [1, 2, 3, 4, 5];
const users = [];
for (const id of userIds) {
const user = await getUser(id); // 5 * 200ms = 1000ms
users.push(user);
}
// Правильно: всі п'ять запитів стартують одночасно
const users = await Promise.all(
userIds.map(id => getUser(id)) // ~200ms разом
);Помилка 4: Розрахунок на автоматичну паралелізацію в Server Components
// Неправильно: все одно послідовно, навіть у Server Component
export default async function Page() {
const user = await getUser(id);
const posts = await getPosts(id); // чекає user, хоча не повинен
return <div>{/* ... */}</div>;
}
// Правильно: явно паралельно
export default async function Page() {
const [user, posts] = await Promise.all([
getUser(id),
getPosts(id)
]);
return <div>{/* ... */}</div>;
}Next.js не змінює порядок твоїх await. Паралелізм - твоя відповідальність.
Помилка 5: Передача вже resolved значень у Promise.all()
// Неправильно: кожен запит вже виконався послідовно
const user = await getUser(id);
const posts = await getPosts(id);
return Promise.all([user, posts]); // це значення, не Promise
// Правильно: передаємо самі Promise
return Promise.all([getUser(id), getPosts(id)]);Де зустрічається в реальних проектах
- Dashboard-сторінки в Next.js: профіль, аналітика, сповіщення - все паралельно перед рендерингом
- React Query:
useQueries()запускає кілька запитів паралельно;useQuery()- один за раз - GraphQL: один HTTP-запит, де кілька полів резолвяться паралельно на сервері
- Express middleware:
Promise.all([checkAuth(), checkRateLimit(), logRequest()])- перевірки разом - Запити до бази даних:
Promise.all([db.users.find(), db.posts.find()])замість послідовних
Питання на співбесіді
Q: Коли використовувати Promise.allSettled() замість Promise.all()?
A: Коли одна помилка не має блокувати все інше. Promise.all() одразу падає якщо будь-який Promise відхилено. allSettled() чекає завершення всіх і повертає статус кожного. Підходить для некритичних даних - рекомендації, аналітика - де збій має показати fallback, а не поламати сторінку.
Q: Чи може бути waterfall всередині паралельного fetch?
A: Так. await Promise.all([getUser(id), getPosts(user.id)]) - другий запит все одно потребує user, щоб стартувати. Рішення: запускати Promise до виклику Promise.all(), або передавати спільний ID який вже є, без залежності від попереднього запиту.
Q: Як Suspense streaming змінює вибір між паралельним і послідовним підходом?
A: З Suspense можна показати частину UI поки повільні дані вантажаться окремо. Критичні дані (профіль) отримуємо паралельно з усім іншим, некритичні (рекомендації) - за межею Suspense. Отримуєш швидкість паралельного підходу з UX поступового рендерингу.
Q: На сторінці 10 незалежних API-запитів, один стабільно займає 3 секунди. Як оптимізувати не змінюючи API?
A: Запустити всі 10 паралельно. Показати 9 швидких результатів через Suspense boundaries поки повільний стримиться у фоні. Або Promise.allSettled() з timeout-wrapper - показати fallback якщо запит перевищив поріг. Головне: не блокувати рендер всієї сторінки через один повільний запит.
Q: Як перевірити що запити реально паралельні?
A: DevTools, вкладка Network - часова шкала. Паралельні запити перекриваються; послідовні стартують тільки після завершення попереднього. На сервері - console.time() / console.timeEnd() навколо Promise.all() і порівняти загальний час із сумою окремих.
Приклади
Базовий: послідовно проти паралельно в Server Component
// Послідовно: posts чекає user, хоча не потребує його даних
export default async function ProfilePage({ userId }: { userId: string }) {
const user = await getUser(userId); // 200ms
const posts = await getUserPosts(userId); // 300ms - міг би стартувати раніше
// Разом: ~500ms
return <div><h1>{user.name}</h1><PostList posts={posts} /></div>;
}
// Паралельно: обидва стартують одночасно
export default async function ProfilePage({ userId }: { userId: string }) {
const [user, posts] = await Promise.all([
getUser(userId), // 200ms
getUserPosts(userId), // 300ms - стартує одразу
]);
// Разом: ~300ms
return <div><h1>{user.name}</h1><PostList posts={posts} /></div>;
}Обидва отримують однакові дані. Паралельна версія швидша на 200ms. При більшій кількості незалежних запитів різниця зростає пропорційно.
Середній рівень: dashboard з трьома незалежними джерелами даних
// Next.js Server Component - всі три запити стартують одночасно
export default async function Dashboard({ userId }: { userId: string }) {
const [user, analytics, notifications] = await Promise.all([
db.user.findUnique({ where: { id: userId } }),
db.analytics.getUserStats(userId),
db.notifications.getUnread(userId)
]);
// Жоден із запитів не залежить від інших - Promise.all робить це явним
return (
<div>
<UserCard user={user} />
<AnalyticsChart data={analytics} />
<NotificationBell count={notifications.length} />
</div>
);
}Жоден із цих запитів не потребує даних іншого. Немає сенсу чекати user перед отриманням аналітики. Promise.all() робить намір явним: всі три незалежні.
Просунутий рівень: змішаний патерн із залежними і незалежними запитами
export default async function ProductPage({ productId }: { productId: string }) {
// Запускаємо всі три Promise одразу.
// reviews і recommendations потребують лише productId, який вже є.
const productPromise = getProduct(productId);
const reviewsPromise = getReviews(productId);
const recommendationsPromise = getRecommendations(productId);
const [product, reviews, recommendations] = await Promise.all([
productPromise,
reviewsPromise,
recommendationsPromise
]);
// Дані продавця залежать від product.sellerId - послідовно тут виправдано
const seller = await getSeller(product.sellerId);
return (
<div>
<ProductCard product={product} seller={seller} />
<ReviewList reviews={reviews} />
<RecommendationGrid items={recommendations} />
</div>
);
}Запуск Promise до Promise.all() - ключовий патерн тут. Якби перший рядок був const product = await getProduct(productId), reviews і recommendations чекали б зайві 300ms без жодної причини. Послідовний fetch для seller в кінці навмисний: він справді потребує product.sellerId.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.