Streaming и Loading UI в Next.js
Streaming позволяет отправлять HTML клиенту по частям, по мере готовности. Вместо того чтобы ждать завершения всех запросов, пользователь сразу видит часть страницы, а остальное подгружается.
Проблема без Streaming
Без streaming весь HTML генерируется целиком до отправки:
Все данные загружаются на сервере
Запрос к БД на список задач, запрос к API за статистикой, запрос на профиль. Все последовательно или параллельно, но HTML отправляется только когда все готово.
Пользователь ждет
Белый экран, пока сервер не соберет всю страницу. TTFB может быть несколько секунд.
Как Streaming решает проблему
С streaming Next.js отправляет части страницы по мере их готовности:
Мгновенный ответ
HTML с layout, навигацией и скелетонами отправляется сразу.
Постепенная загрузка
Данные подгружаются по мере готовности. Каждый блок заменяет свой скелетон.
loading.tsx
Самый простой способ добавить loading UI. Next.js автоматически оборачивает page.tsx в React Suspense с fallback'ом из loading.tsx:
// app/problems/loading.tsx
export default function Loading() {
return (
<div className="space-y-4">
{[...Array(5)].map((_, i) => (
<div
key={i}
className="h-16 bg-neutral-200 animate-pulse rounded-lg"
/>
))}
</div>
)
}
Пользователь видит скелетон сразу, а контент появляется по мере загрузки данных.
Suspense для гранулярного контроля
Для более точного контроля используй Suspense напрямую:
// app/dashboard/page.tsx
import { Suspense } from 'react'
export default function DashboardPage() {
return (
<div>
<h1>Дашборд</h1>
<Suspense fallback={<StatsSkeleton />}>
<UserStats />
</Suspense>
<Suspense fallback={<ActivitySkeleton />}>
<RecentActivity />
</Suspense>
<Suspense fallback={<ProgressSkeleton />}>
<RoadmapProgress />
</Suspense>
</div>
)
}
Каждый Suspense boundary стримится независимо. UserStats может появиться раньше RecentActivity.
Как это работает под капотом
// 1. Сервер отправляет HTML с fallback'ами
<div>
<h1>Дашборд</h1>
<div id="stats"><!-- StatsSkeleton --></div>
<div id="activity"><!-- ActivitySkeleton --></div>
</div>
// 2. Когда данные готовы, сервер отправляет <script>
// который заменяет fallback на реальный контент
<script>
$RC("stats", "<div>Решено задач: 42</div>")
</script>
Браузер получает поток данных по одному HTTP-соединению.
Паттерн: серверный компонент с async
Suspense работает с async серверными компонентами:
// Этот компонент будет стримиться
async function UserStats() {
const stats = await db.user.getStats(userId)
// Запрос может занять 2 секунды
// Пользователь увидит скелетон, пока данные загружаются
return (
<div>
<p>Решено задач: {stats.problemsSolved}</p>
<p>Прочитано статей: {stats.docsRead}</p>
</div>
)
}
loading.tsx vs Suspense
| loading.tsx | Suspense | |
|---|---|---|
| Область | Вся страница (page.tsx) | Конкретный компонент |
| Гранулярность | Один fallback на страницу | Несколько независимых |
| Настройка | Автоматическая | Ручная |
Практический совет:
Используй loading.tsx для простых страниц с одним блоком данных. Для дашбордов и сложных страниц используй Suspense для каждого блока отдельно, чтобы контент появлялся по мере готовности.
Полезные ресурсы
- nextjs.org/docs — Loading UI and Streaming
- react.dev — Suspense