Loading UI and streaming
While a server component waits for data, the visitor sees a blank screen. To avoid that, Next.js can send the page in parts: first whatever is ready plus a placeholder where the slow part goes, then the missing piece. This approach is called streaming.
Slow data
As an experiment, let us make a comments component that takes two seconds to load. Create components/Comments.tsx:
// components/Comments.tsx
export default async function Comments() {
await new Promise((resolve) => setTimeout(resolve, 2000));
const comments = ['Oleg: Great post!', 'Anna: Thank you!'];
return (
<ul>
{comments.map((text) => (
<li key={text}>{text}</li>
))}
</ul>
);
}The delay imitates a slow database query. If you put <Comments /> into the post page as usual, the whole page takes two seconds to open.
The loading.tsx file
The simplest way is a loading.tsx file next to the page:
// app/posts/[slug]/loading.tsx
export default function Loading() {
return <p>Loading the post...</p>;
}While the post page is being prepared, "Loading the post..." is shown in its place. Layouts — the header and menu — appear immediately; the placeholder replaces only the page content.
Suspense for part of a page
loading.tsx replaces the whole page. To show the post at once and wait only for the comments, wrap the slow component in React's Suspense:
// app/posts/[slug]/page.tsx
import { Suspense } from 'react';
import { notFound } from 'next/navigation';
import Comments from '@/components/Comments';
import { getPost } from '@/lib/posts';
export default async function PostPage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const post = await getPost(slug);
if (!post) notFound();
return (
<article>
<h1>{post.title}</h1>
<p>{post.body}</p>
<Suspense fallback={<p>Loading comments...</p>}>
<Comments />
</Suspense>
</article>
);
}Open /posts/hello-nextjs: the post is visible at once, below it "Loading comments...", and two seconds later the list appears in that spot.
How it fits together
| Approach | What the placeholder replaces | Where it is written |
|---|---|---|
loading.tsx | The whole page of the folder | A file next to page.tsx |
Suspense | Only the wrapped part | Inside the markup |
The fallback prop is what to show instead of the content until it is ready. A page can have several Suspense blocks, and each one appears independently.
Common mistake
Loading the slow data in the page itself with await and then passing it to a component inside Suspense. The page still waits, because the await is above. The slow request must run inside the component wrapped in Suspense, like Comments in the example.