Skip to main content

Паралельне та послідовне отримання даних у Next.js

Паралельне отримання даних запускає всі запити одразу і чекає найповільнішого; послідовне чекає завершення кожного запиту перед тим, як почати наступний.

Теорія

TL;DR

  • Послідовно = waterfall: 200ms + 300ms + 200ms = 700ms разом
  • Паралельно = Promise.all(): max(200ms, 300ms, 200ms) = 300ms разом
  • Паралельно - коли запити незалежні; послідовно - коли пізніший запит потребує результат попереднього
  • Promise.all() завершується з помилкою якщо будь-який запит падає; для часткових збоїв використовуй Promise.allSettled()
  • Server Components у Next.js не паралелізують запити автоматично - структуру треба прописувати самостійно

Швидкий приклад

tsx
// Послідовно: усі затримки складаються 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 всередині нібито паралельного коду

tsx
// Неправильно: 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() падає при будь-якій помилці

tsx
// Неправильно: якщо 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)

tsx
// Неправильно: по одному користувачу за раз 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

tsx
// Неправильно: все одно послідовно, навіть у 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()

tsx
// Неправильно: кожен запит вже виконався послідовно 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

tsx
// Послідовно: 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 з трьома незалежними джерелами даних

tsx
// 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() робить намір явним: всі три незалежні.

Просунутий рівень: змішаний патерн із залежними і незалежними запитами

tsx
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.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?