Suspense境界とコンポーネント単位のストリーミング
低速なデータコンポーネントをSuspenseで囲み、静的シェルとは独立してストリーミングします。
「Suspense境界とコンポーネント単位のストリーミング」はCoddyKit上の無料Next.js 15 Fullstack (App Router + Server Actions)レッスンです。 これはレッスン1/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはNext.js 15 Fullstack (App Router + Server Actions)学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 Next.js 15 Fullstack (App Router + Server Actions)コースには全4レッスンが含まれています。
このレッスンの一部はまだ翻訳されておらず、英語で表示されています。
Why Stream at the Component Level?
In the App Router, a page can contain both fast static content (header, nav, layout shell) and slow data-dependent content (a dashboard widget that hits a third-party API).
Without streaming, the whole page waits for the slowest fetch before anything renders. That hurts perceived performance.
Component-level streaming lets Next.js send the static shell immediately, then stream each slow part in as its data resolves. The tool that enables this is React's <Suspense> boundary.
The Blocking Problem
Here a single async Server Component awaits a slow query. Because the page awaits before returning JSX, the user sees nothing until the 2-second fetch finishes.
The fast parts of the page (title, layout) are held hostage by the slow part.
// app/dashboard/page.tsx
async function getStats() {
// simulates a slow 2s upstream call
await new Promise((r) => setTimeout(r, 2000));
return { revenue: 4200, orders: 87 };
}
export default async function DashboardPage() {
const stats = await getStats(); // blocks the WHOLE page
return (
<main>
<h1>Dashboard</h1>
<p>Revenue: {stats.revenue}</p>
<p>Orders: {stats.orders}</p>
</main>
);
}Extract the Slow Part into Its Own Component
The first step to streaming is isolation: move the awaited data into a separate async Server Component.
The page itself no longer awaits anything, so its static shell can render instantly. The slow work now lives inside <Stats />.
// app/dashboard/stats.tsx
async function getStats() {
await new Promise((r) => setTimeout(r, 2000));
return { revenue: 4200, orders: 87 };
}
export async function Stats() {
const stats = await getStats();
return (
<section>
<p>Revenue: {stats.revenue}</p>
<p>Orders: {stats.orders}</p>
</section>
);
}Wrap It in a Suspense Boundary
Now wrap the slow component in <Suspense> and give it a fallback. Next.js renders the shell plus the fallback immediately, then streams the real component over the same HTTP response once its data resolves.
fallbackshows while the boundary's data is pending.- Everything outside the boundary is sent right away.
// app/dashboard/page.tsx
import { Suspense } from 'react';
import { Stats } from './stats';
export default function DashboardPage() {
return (
<main>
<h1>Dashboard</h1> {/* sent immediately */}
<Suspense fallback={<p>Loading stats…</p>}>
<Stats /> {/* streamed in when ready */}
</Suspense>
</main>
);
}A Good Fallback Is a Skeleton
The fallback should match the shape of the final content to avoid layout shift. A skeleton placeholder is far better than a bare spinner because it reserves space and signals what's coming.
Keep skeletons as plain, fast Client or Server Components with no data dependencies.
// app/dashboard/stats-skeleton.tsx
export function StatsSkeleton() {
return (
<section aria-hidden className="animate-pulse">
<div className="h-6 w-40 rounded bg-gray-200" />
<div className="mt-2 h-6 w-32 rounded bg-gray-200" />
</section>
);
}
// usage:
// <Suspense fallback={<StatsSkeleton />}>
// <Stats />
// </Suspense>Multiple Independent Boundaries
Each <Suspense> streams independently. If you have several slow widgets, give each its own boundary so a slow one never blocks a fast one.
Below, RecentOrders may resolve in 300ms while Revenue takes 2s — and each appears the moment it's ready, in any order.
import { Suspense } from 'react';
import { Revenue } from './revenue';
import { RecentOrders } from './recent-orders';
import { RevenueSkeleton, OrdersSkeleton } from './skeletons';
export default function DashboardPage() {
return (
<main>
<h1>Dashboard</h1>
<Suspense fallback={<RevenueSkeleton />}>
<Revenue />
</Suspense>
<Suspense fallback={<OrdersSkeleton />}>
<RecentOrders />
</Suspense>
</main>
);
}Boundary Granularity Is a Design Choice
You decide how to group slow components under boundaries:
- One boundary per widget → each widget pops in on its own (best for unrelated data).
- One boundary around a group → the group appears together once all its data resolves (good when a coordinated reveal looks cleaner).
A shared boundary streams only when the slowest child inside it is ready, so don't accidentally couple a fast widget to a slow one.
Passing Promises Down with the `use` Hook
An alternative pattern: start the fetch in the parent without awaiting, then pass the promise to a child that unwraps it with React's use hook. The child must be inside a <Suspense> boundary, which suspends on the pending promise.
This lets the parent kick off several requests in parallel before any of them block.
// app/dashboard/page.tsx
import { Suspense } from 'react';
import { Stats } from './stats';
function getStats() {
return new Promise<{ revenue: number }>((r) =>
setTimeout(() => r({ revenue: 4200 }), 2000),
);
}
export default function Page() {
const statsPromise = getStats(); // NOT awaited
return (
<Suspense fallback={<p>Loading…</p>}>
<Stats statsPromise={statsPromise} />
</Suspense>
);
}The Client Component That Reads the Promise
The child uses use(promise) to read the resolved value. When the promise is pending, use suspends and the nearest <Suspense> shows its fallback.
use can be called in a Client Component, making it the idiomatic way to stream a server-started promise into interactive UI.
// app/dashboard/stats.tsx
'use client';
import { use } from 'react';
export function Stats({
statsPromise,
}: {
statsPromise: Promise<{ revenue: number }>;
}) {
const stats = use(statsPromise); // suspends until resolved
return <p>Revenue: {stats.revenue}</p>;
}loading.tsx Is a Route-Level Suspense
A file named loading.tsx in a route segment is sugar: Next.js automatically wraps that segment's page.tsx in a <Suspense> using the loading file as the fallback.
- loading.tsx → streams the whole page while it loads (one big boundary).
- Manual <Suspense> → streams parts of the page independently.
Use loading.tsx for the coarse first paint, and inline <Suspense> for fine-grained component streaming inside the page.
// app/dashboard/loading.tsx
export default function Loading() {
return <p>Loading dashboard…</p>;
}Don't Forget: Pure Functions Can Be Tested in Isolation
The data-shaping logic that feeds your streamed components is just plain TypeScript — keep it pure so you can unit-test it without a server. Here a standalone summarizer that any judge can run.
type Order = { id: number; total: number };
function summarize(orders: Order[]): { count: number; revenue: number } {
const revenue = orders.reduce((sum, o) => sum + o.total, 0);
return { count: orders.length, revenue };
}
const orders: Order[] = [
{ id: 1, total: 1200 },
{ id: 2, total: 3000 },
];
const result = summarize(orders);
console.log(`Orders: ${result.count}, Revenue: ${result.revenue}`);Quick Check
You have a dashboard page with a fast header and two slow widgets: <Revenue /> (~2s) and <Orders /> (~300ms). You want the header to appear instantly and each widget to appear the moment its own data is ready, independently.
Recap
Key takeaways for component-level streaming:
- Isolate slow data fetches into their own async Server Components.
- Wrap each in
<Suspense fallback={…}>— the shell and everything outside the boundary stream immediately. - Use skeleton fallbacks that match final shape to avoid layout shift.
- Multiple boundaries stream independently; a shared boundary waits for its slowest child.
- Pass an un-awaited promise down and read it with the
usehook for parallel, streamed data. loading.tsxis an automatic route-level Suspense for coarse first paint; inline<Suspense>handles fine-grained streaming.
よくある質問
「Suspense境界とコンポーネント単位のストリーミング」レッスンは無料ですか?
はい。「Suspense境界とコンポーネント単位のストリーミング」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、Next.js 15 Fullstack (App Router + Server Actions)コースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 Next.js 15 Fullstack (App Router + Server Actions)コースには全4レッスンが含まれています。
「Suspense境界とコンポーネント単位のストリーミング」で何を学びますか?
低速なデータコンポーネントをSuspenseで囲み、静的シェルとは独立してストリーミングします。 ブラウザで直接実行するハンズオンコードでNext.js 15 Fullstack (App Router + Server Actions)を演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。
Next.js 15 Fullstack (App Router + Server Actions)を始めるのに経験は必要ですか?
事前経験は必要ありません。CoddyKitのNext.js 15 Fullstack (App Router + Server Actions)は初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン1/4です。
「Suspense境界とコンポーネント単位のストリーミング」レッスンにはどのくらい時間がかかりますか?
ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。
このNext.js 15 Fullstack (App Router + Server Actions)レッスンでコードを書いて実行できますか?
はい。すべてのNext.js 15 Fullstack (App Router + Server Actions)レッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。
このコースのすべてのレッスン
- Suspense境界とコンポーネント単位のストリーミング
- 意味のあるloading.tsxとスケルトンの作成
- 部分的プリレンダリング:静的シェルと動的な穴
- ストリーミングの落とし穴:レイアウトシフトとウォーターフォール