Optimizing data fetching in Next.js
Embracing React Server Components is a new way of presenting UI to your users. Next.js 15 and React 19 leverage streaming optimizations for intelligent delivery to the browser. Next.js provides granular control of delivering the dynamic personalized content to your users. Here are some quick tips to get your pages opitimized for streaming.
Using layout.tsx
Introduce a loading.tsx
. It is a quick win as it adds a React.Suspense
boundary for your page.tsx
as a whole. This means that if your page fetches data and there are no other React.Suspense
boundaries around them. This boundary will engage and present to the user until the async processing has completed, which will then replace the fallback
content with the correct page content.
This is a great baseline because it will at least navigate and show the user something is happening when Next is routing from page to page.
// /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 = await 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
To make the experience even better, move your data fetching closer to the UI. The Page
then becomes static-like with the "route personalized" content and puts the data fetching inside of the UI that is presenting that information (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() {
return (
<main>
<h1>Users</h1>
<Suspense fallback={<div>loading users...</div>}>
<UsersTable />;
</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 this to a dashboard or a more complex page that has different types of content, you can separate your UI into streamable chunks that each have their own built-on loading states.
Using Suspense and React.use
In React 19 there is the new React.use hook for consuming promises. You can create the promise at the <Page />
and pass it to a client component. That component can use React.use
to deserialize the promise and use the return value of that executed promise. Honestly the coolest part here is that it is all async
without any await
or async
components. This is a pattern that allows the Page
RSC to "prefetch" the content while the UI is being returned to the browser.
// no async Page
export default function Page() {
// notice no `await`
const usersPromise = getUsers();
return (
<main>
<h1>Users</h1>
<Suspense fallback={<div>loading users...</div>}>
<UsersTable getUsers={usersPromise} />
</Suspense>
</main
)
}
// @/components/users-table.tsx
"use client";
interface UsersTableProps {
getUsers: Promise<User[]>
}
// Notice the "use client", this is a client component
export function UsersTable({ getUsers }: UsersTableProps) {
const users = React.use(getUsers);
return <table>...</table>;
}