Пошук уроків, статей та іншого контенту
Порівняєте послідовні й паралельні запити та навчитеся оптимізувати час завантаження сторінки.
Послідовні запити виконуються один за одним: наступний запит починається лише після завершення попереднього.
Такий підхід потрібен, коли другий запит залежить від результату першого. Наприклад, спочатку потрібно отримати користувача, а потім завантажити його замовлення:
// app/orders/page.tsx
type User = {
id: number;
name: string;
};
type Order = {
id: number;
total: number;
};
async function getUser(): Promise<User> {
const response = await fetch('https://api.example.com/user/1');
if (!response.ok) {
throw new Error('Не вдалося завантажити користувача');
}
return response.json();
}
async function getOrders(userId: number): Promise<Order[]> {
const response = await fetch(
`https://api.example.com/users/${userId}/orders`,
);
if (!response.ok) {
throw new Error('Не вдалося завантажити замовлення');
}
return response.json();
}
export default async function OrdersPage() {
const user = await getUser();
const orders = await getOrders(user.id);
return (
<main>
<h1>Замовлення користувача {user.name}</h1>
<ul>
{orders.map((order) => (
<li key={order.id}>
Замовлення #{order.id}: {order.total} грн
</li>
))}
</ul>
</main>
);
}У цьому прикладі запити мають залежність:
getUser() повертає користувача.
З отриманого користувача береться user.id.
getOrders(user.id) використовує цей ідентифікатор.
Якщо перший запит триває 500 мс, а другий — 700 мс, загальний час очікування буде приблизно 1200 мс:
500 мс + 700 мс = 1200 мсЗробити такі запити паралельними неможливо, якщо для другого запиту справді потрібен результат першого.
Паралельні запити запускаються одночасно. Це корисно, коли запити незалежні один від одного.
Наприклад, сторінці потрібні профіль користувача, список товарів і сповіщення. Жоден із цих запитів не потребує результату іншого:
// app/dashboard/page.tsx
type User = {
name: string;
};
type Product = {
id: number;
name: string;
};
type Notification = {
id: number;
message: string;
};
async function getUser(): Promise<User> {
const response = await fetch('https://api.example.com/user/1');
if (!response.ok) {
throw new Error('Не вдалося завантажити профіль');
}
return response.json();
}
async function getProducts(): Promise<Product[]> {
const response = await fetch('https://api.example.com/products');
if (!response.ok) {
throw new Error('Не вдалося завантажити товари');
}
return response.json();
}
async function getNotifications(): Promise<Notification[]> {
const response = await fetch('https://api.example.com/notifications');
if (!response.ok) {
throw new Error('Не вдалося завантажити сповіщення');
}
return response.json();
}
export default async function DashboardPage() {
const [user, products, notifications] = await Promise.all([
getUser(),
getProducts(),
getNotifications(),
]);
return (
<main>
<h1>Вітаємо, {user.name}</h1>
<h2>Товари</h2>
<ul>
{products.map((product) => (
<li key={product.id}>{product.name}</li>
))}
</ul>
<h2>Сповіщення</h2>
<ul>
{notifications.map((notification) => (
<li key={notification.id}>{notification.message}</li>
))}
</ul>
</main>
);
}Promise.all() одразу запускає всі передані проміси й очікує на завершення всіх:
const results = await Promise.all([
firstRequest(),
secondRequest(),
thirdRequest(),
]);Якщо запити тривають 500, 700 і 300 мс, загальний час буде приблизно 700 мс — за часом найдовшого запиту:
max(500 мс, 700 мс, 300 мс) = 700 мсУ послідовному варіанті це було б:
500 мс + 700 мс + 300 мс = 1500 мсPromise.all() у Server ComponentsУ Next.js Server Components можуть бути асинхронними. Це дає змогу виконувати запити безпосередньо в компоненті або в окремих функціях.
Ось коротший варіант паралельного завантаження:
// app/page.tsx
async function getPosts() {
const response = await fetch('https://api.example.com/posts');
if (!response.ok) {
throw new Error('Не вдалося завантажити публікації');
}
return response.json();
}
async function getCategories() {
const response = await fetch('https://api.example.com/categories');
if (!response.ok) {
throw new Error('Не вдалося завантажити категорії');
}
return response.json();
}
export default async function HomePage() {
const postsPromise = getPosts();
const categoriesPromise = getCategories();
const [posts, categories] = await Promise.all([
postsPromise,
categoriesPromise,
]);
return (
<main>
<h1>Публікації</h1>
<p>Кількість публікацій: {posts.length}</p>
<p>Кількість категорій: {categories.length}</p>
</main>
);
}Тут важливо, що функції викликаються до await Promise.all(). Обидва запити починають виконуватися одразу.
Цей варіант також коректний:
const [posts, categories] = await Promise.all([
getPosts(),
getCategories(),
]);Використовуйте його, якщо додаткові проміси не потрібні в іншій логіці компонента.
Не кожну послідовність потрібно оптимізувати. Якщо між операціями існує залежність, послідовне виконання є правильним:
const project = await getProject(projectId);
const tasks = await getTasks(project.id);Також послідовність потрібна, якщо:
другий запит використовує токен, отриманий першим;
другий запит потребує ідентифікатор із результату першого;
наступну операцію можна виконати лише після успішного завершення попередньої;
порядок виконання важливий для коректності даних.
Не слід штучно запускати залежні запити паралельно. Це ускладнить код або призведе до помилок.
const user = await getUser();
const posts = await getPosts();
const comments = await getComments();Переваги:
простий для читання код;
зручно працювати із залежностями;
помилки легко відстежувати.
Недолік:
незалежні запити створюють зайве очікування.
const [user, posts, comments] = await Promise.all([
getUser(),
getPosts(),
getComments(),
]);Переваги:
менший загальний час завантаження;
усі незалежні запити стартують одразу;
зручно завантажувати дані для різних частин сторінки.
Недолік:
якщо один проміс відхилиться, Promise.all() відхилиться також.
Promise.all() завершується помилкою, якщо хоча б один із його промісів завершується з помилкою:
try {
const [user, posts] = await Promise.all([
getUser(),
getPosts(),
]);
// Робота з отриманими даними
} catch (error) {
console.error('Помилка завантаження даних:', error);
}У Next.js помилку також можна обробляти через спеціальний файл error.tsx у відповідному сегменті маршруту. Однак для оптимізації запитів важливо розуміти саме поведінку промісів:
Promise.all() очікує успішного завершення всіх операцій;
помилка одного запиту робить всю групу невдалою;
якщо для різних частин сторінки потрібна незалежна обробка помилок, запити можна обробляти окремо.
Наприклад:
const [userResult, postsResult] = await Promise.allSettled([
getUser(),
getPosts(),
]);
if (userResult.status === 'rejected') {
console.error('Не вдалося завантажити користувача');
}
if (postsResult.status === 'rejected') {
console.error('Не вдалося завантажити публікації');
}Promise.allSettled() чекає завершення всіх промісів незалежно від того, успішні вони чи ні. Для звичайного випадку, коли без усіх даних сторінка не може бути показана, достатньо Promise.all().
У Next.js різні частини сторінки можуть бути окремими асинхронними Server Components. Це дає змогу не обов’язково блокувати всю сторінку одним повільним запитом.
Наприклад, компонент статистики може завантажувати власні дані:
// app/dashboard/page.tsx
import { Suspense } from 'react';
async function Statistics() {
const response = await fetch('https://api.example.com/statistics');
if (!response.ok) {
throw new Error('Не вдалося завантажити статистику');
}
const statistics = await response.json();
return (
<section>
<h2>Статистика</h2>
<p>Продажі: {statistics.sales}</p>
</section>
);
}
async function RecentOrders() {
const response = await fetch('https://api.example.com/orders/recent');
if (!response.ok) {
throw new Error('Не вдалося завантажити замовлення');
}
const orders = await response.json();
return (
<section>
<h2>Останні замовлення</h2>
<p>Кількість: {orders.length}</p>
</section>
);
}
export default function DashboardPage() {
return (
<main>
<h1>Панель керування</h1>
<Suspense fallback={<p>Завантаження статистики...</p>}>
<Statistics />
</Suspense>
<Suspense fallback={<p>Завантаження замовлень...</p>}>
<RecentOrders />
</Suspense>
</main>
);
}У цьому прикладі Statistics і RecentOrders не залежать один від одного. Вони можуть завантажувати дані паралельно, а кожна частина інтерфейсу має власний стан завантаження.
Такий підхід особливо корисний, коли:
одна частина сторінки швидка, а інша повільна;
не потрібно чекати на всі дані перед показом інтерфейсу;
для різних блоків потрібні різні повідомлення завантаження.
Під час перевірки Server Component звертайте увагу на кілька await, записаних підряд:
const first = await getFirst();
const second = await getSecond();
const third = await getThird();Поставте собі запитання:
Чи залежить getSecond() від first?
Чи залежить getThird() від second?
Чи можна запустити ці запити одночасно?
Якщо залежностей немає, перепишіть код:
const [first, second, third] = await Promise.all([
getFirst(),
getSecond(),
getThird(),
]);Водночас не потрібно об'єднувати в Promise.all() запити лише заради самого об'єднання. Головний критерій — незалежність операцій.
const user = await getUser();
const posts = await getPosts();Якщо getPosts() не потребує user, такий код створює зайву затримку. Використовуйте Promise.all().
Promise.all() для залежних запитівconst [user, orders] = await Promise.all([
getUser(),
getOrders(user.id),
]);Цей код некоректний, оскільки змінна user ще не існує в момент формування масиву промісів. Залежні запити потрібно виконувати послідовно:
const user = await getUser();
const orders = await getOrders(user.id);const posts = await [getPosts(), getFeaturedPosts()];await не перетворює масив промісів на масив результатів. Для цього потрібен Promise.all():
const [posts, featuredPosts] = await Promise.all([
getPosts(),
getFeaturedPosts(),
]);Promise.all() без обробки помилкиЯкщо один запит може завершитися помилкою, передбачте обробку на рівні компонента або маршруту. Інакше вся група запитів буде вважатися невдалою.
Паралельність підходить не лише для читання, але операції зі зміною даних можуть залежати від порядку. Перед використанням Promise.all() переконайтеся, що одночасне виконання не порушує логіку застосунку.
Знайдіть усі запити, які виконує сторінка.
Визначте залежності між ними.
Залиште залежні запити послідовними.
Об'єднайте незалежні запити через Promise.all().
Для повільних незалежних блоків розгляньте окремі Server Components із Suspense.
Перевірте обробку помилок для всієї групи запитів.
Порівняйте час завантаження до та після змін.
Головне правило:
Незалежні запити запускайте паралельно, а залежні виконуйте послідовно.
Послідовні запити виконуються один за одним.
Послідовність необхідна, якщо наступний запит залежить від результату попереднього.
Незалежні запити варто запускати через Promise.all().
Час паралельного виконання приблизно дорівнює тривалості найдовшого запиту, а не сумі всіх запитів.
Promise.all() відхиляється, якщо відхиляється хоча б один проміс.
Окремі асинхронні Server Components і Suspense дають змогу поступово показувати частини сторінки.
Оптимізація запитів починається з аналізу їхніх залежностей.