Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Паралельне та послідовне отримання даних у Next.js». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Паралельне отримання даних у Next.js** запускає кілька запитів одночасно і чекає найповільнішого. Послідовне виконує по одному, блокуючи кожен наступний до завершення попереднього. ```tsx // Послідовно: 200ms + 300ms = 500ms разом const user = await getUser(id); const posts = await getPosts(id); // Паралельно: max(200ms, 300ms) = 300ms разом const [user, posts] = await Promise.all([getUser(id), getPosts(id)]); ``` **Головне:** Server Components не паралелізують запити автоматично. Для незалежних запитів використовуй `Promise.all()` явно.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Паралельне отримання даних** запускає всі запити одразу і чекає найповільнішого; послідовне чекає завершення кожного запиту перед тим, як почати наступний. ## Теорія ### 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`.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.