Streaming + Suspense cheatsheet.
What it does
Server starts sending HTML before all data is ready. Browser shows shell + fallbacks; data fills in as it arrives.
Result: lower TTFB, faster FCP, better perceived speed.
loading.tsx (route-level)
// app/posts/loading.tsx
export default function Loading() {
return <Skeleton />;
}
Wraps the segment in Suspense automatically. Shows while server-side data fetches.
Manual Suspense
// app/page.tsx
import { Suspense } from "react";
export default function Page() {
return (
<>
<Header />
<Suspense fallback={<UserSkel />}>
<User />
</Suspense>
<Suspense fallback={<FeedSkel />}>
<Feed />
</Suspense>
</>
);
}
async function User() {
const u = await fetchUser();
return <p>{u.name}</p>;
}
async function Feed() {
const f = await fetchFeed();
return <ul>{f.map(...)}</ul>;
}
<Header> flushes immediately. User and Feed stream independently.
Parallel fetching
// BAD: sequential
async function Page() {
const user = await fetchUser();
const posts = await fetchPosts();
...
}
// GOOD: parallel
async function Page() {
const userP = fetchUser();
const postsP = fetchPosts();
const [user, posts] = await Promise.all([userP, postsP]);
...
}
Even better: separate async components in separate Suspense boundaries.
Streaming with separate components
export default function Page() {
return (
<>
<Suspense fallback={<UserSkel />}>
<User /> {/* fetches user */}
</Suspense>
<Suspense fallback={<PostsSkel />}>
<Posts /> {/* fetches posts */}
</Suspense>
</>
);
}
Each component starts its fetch immediately, in parallel, and streams when ready.
Avoiding waterfall
// BAD: child waits for parent's fetch
async function Parent() {
const data = await fetchParent();
return <Child data={data} />;
}
async function Child({ data }) {
const more = await fetchChild(data.id); // sequential
}
// GOOD: kick off both fetches early
async function Page() {
const parentP = fetchParent();
const parent = await parentP;
const childP = fetchChild(parent.id); // can start here
const child = await childP;
}
If you can’t parallelize (child depends on parent), at least stream independently.
error.tsx + Suspense
// app/posts/error.tsx
"use client";
export default function Error({ error, reset }) {
return <p>error: {error.message}</p>;
}
Catches errors thrown from suspended children.
Nested Suspense
<Suspense fallback={<PageSkel />}>
<Layout>
<Suspense fallback={<SidebarSkel />}>
<Sidebar />
</Suspense>
<Suspense fallback={<MainSkel />}>
<Main />
</Suspense>
</Layout>
</Suspense>
Outer shell renders first, then nested fall back to skeletons, then content.
Streaming with Server Actions
async function action() {
"use server";
await saveData();
revalidatePath("/");
}
Stream + actions compose: page renders with old data, action triggers, revalidate streams new HTML.
Skeleton design
function UserSkel() {
return (
<div className="space-y-2">
<div className="h-4 w-32 bg-gray-200 animate-pulse rounded" />
<div className="h-3 w-48 bg-gray-200 animate-pulse rounded" />
</div>
);
}
Match dimensions of real content to avoid layout shift.
When Suspense doesn’t help
- Sequential data dependency (waterfall) — refactor first.
- Fast fetches (< 100ms) — shell+fallback flicker isn’t worth it.
- Static pages — no benefit; content is ready immediately.
Suspense + client components
"use client";
import { useSuspenseQuery } from "@tanstack/react-query";
function User({ id }: { id: number }) {
const { data } = useSuspenseQuery({
queryKey: ["user", id],
queryFn: () => fetchUser(id),
});
return <p>{data.name}</p>;
}
Wrap in <Suspense> higher up. Throws-on-pending integrates with React’s mechanism.
Streaming and OG/Twitter
Crawlers usually wait for the full response. Streaming doesn’t affect SEO since servers still send 200 OK + full HTML eventually.
Common mistakes
- One giant Suspense at root — all-or-nothing loading.
- Skeleton with different dimensions than content → CLS.
- Forgetting error.tsx — uncaught suspense errors crash everything.
- Sequential await chain → no streaming benefit.
- Async client component (not allowed; use
useSuspenseQueryoruse()).
Read this next
If you want my streaming + suspense templates, they’re at rajpoot.dev .
Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .