Streaming и Suspense
Streaming — отправка HTML по частям, по мере готовности.
Проблема
Без streaming страница ждёт все данные, потом отправляет HTML. Если один запрос медленный — пользователь видит белый экран.
Решение: Suspense
import { Suspense } from 'react';
export default function Page() {
return (
<>
<Header /> {/* сразу */}
<Suspense fallback={<Skeleton />}>
<SlowComponent /> {/* позже */}
</Suspense>
</>
);
}
async function SlowComponent() {
const data = await fetchSlowData();
return <div>{data}</div>;
}
Пользователь видит Header и Skeleton сразу, SlowComponent — по мере готовности.
Несколько Suspense
<>
<Header />
<Suspense fallback={<PostsSkeleton />}>
<Posts />
</Suspense>
<Suspense fallback={<CommentsSkeleton />}>
<Comments />
</Suspense>
<Suspense fallback={<SidebarSkeleton />}>
<Sidebar />
</Suspense>
</>
Каждый блок грузится независимо.
loading.tsx
Автоматический Suspense для страницы:
// app/posts/loading.tsx
export default function Loading() {
return <PostsSkeleton />;
}
Показывается, пока страница грузится.
Вложенные Suspense
<Suspense fallback={<PageSkeleton />}>
<Header />
<Suspense fallback={<ListSkeleton />}>
<List />
</Suspense>
<Suspense fallback={<SidebarSkeleton />}>
<Sidebar />
</Suspense>
</Suspense>
Внешний показывается первым, внутренние — по мере готовности.
Когда использовать
- Медленные запросы к БД
- Внешние API
- Тяжёлые вычисления
- Персонализированные блоки
Что не нужно оборачивать
- Быстрые компоненты
- Статические части
- Всё, что и так быстро
Скелетоны
function PostsSkeleton() {
return (
<div className="skeleton">
{[...Array(5)].map((_, i) => (
<div key={i} className="skeleton-item" />
))}
</div>
);
}
Streaming в API
// app/api/stream/route.ts
export async function GET() {
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
for (let i = 0; i < 10; i++) {
controller.enqueue(encoder.encode(`chunk ${i}\n`));
await new Promise((r) => setTimeout(r, 100));
}
controller.close();
},
});
return new Response(stream, {
headers: { 'Content-Type': 'text/plain' },
});
}
AI-стриминг
Vercel AI SDK стримит ответы модели:
import { streamText } from 'ai';
import { openai } from '@ai-sdk/openai';
export async function POST(req: Request) {
const { prompt } = await req.json();
const result = streamText({
model: openai('gpt-4'),
prompt,
});
return result.toDataStreamResponse();
}
Клиент получает токены по мере генерации.
Suspense и use
import { use } from 'react';
function Comments({ commentsPromise }: { commentsPromise: Promise<Comment[]> }) {
const comments = use(commentsPromise); // разворачивает промис
return <ul>{comments.map((c) => <li key={c.id}>{c.text}</li>)}</ul>;
}
// В родителе
const commentsPromise = getComments();
<Suspense fallback={<Skeleton />}>
<Comments commentsPromise={commentsPromise} />
</Suspense>
use — новый хук React для промисов в клиентских компонентах.
Итоги
- Streaming отправляет HTML по частям
Suspenseоборачивает медленные блокиloading.tsx— автоматический Suspense- Скелетоны для UX
- AI-ответы стримятся через AI SDK
useдля промисов в клиентских