Optimizing a next.js page for streaming
Next.js 15 leans deeper into streaming optimizations, allowing for faster delivery to the browser, and more granular control of how your page renders it's content. Here are some quick tips to get your pages opitimized.
Using layout.tsx
Sometimes data is required before a page itself can be rendered. This could be expirimentation like feature flags, maybe personalization data, or authorization checks. When this data fetch happens in a React Server Component (RSC), this blocks the page from returning a response, thus blocking rendering. This makes the user see... nothing, until the data has been retrieved. Creating a layout.tsx
allows the page "boundary" to be wrapped in a suspense, allowing the layout and the structure wrapping the page to render, showing and sort of page-level loading state.
// /app/users/loading.tsx
export default function Loading() {
return <div>Page is loading...</div>;
}
// /app/users/page.tsx
export default async function Page() {
const users = getUsers();
return <UsersTable users={users} />;
}
While the getUsers
executes, the content in loading.tsx
will display. Once this page has it's users, the new content will be replaced with the UsersTable
.
Using <Suspense>
for more control
Another common pattern is to fetch your data in children
Server Components of a <Page />
. This allows the Page
to handle less work and puts the data fetching or computations closer to the output rendered from each component (keep in mind that you can reuse fetch responses). You can then wrap your RSC in a <Suspense>
boundary which will allow the component to show its "loading state" without blocking the Page
itself from rendering.
// /app/users/page.tsx
import UsersTable from "@/components/users-table";
// NOTICE - the page is not async
export default function Page() {
const users = getUsers();
return (
<main>
<h1>Users</h1>
<Suspense fallback={<div>loading users...</div>}>
<UsersTable users={users} />;
</Suspense>
</main>
);
}
// @/components/users-table.tsx
export default async function UsersTable() {
const users = await getUsers();
return <table>...</table>;
}
With this, you'll see the users page immediately, with the <h1>
rendered, but if the user data hasn't completed, it will show the <div>
with "loading users..." until the data has been fetched, which then it would replace with the <table>
content.
Scale to a dashboard or a more complex page that has different types of content loading, you can separate your UI into streamable chunks that each have their own built-on loading states.