skip to content
Samuel Edwin's Website

How to speed up page renders in Next.js with Suspense

/ 3 min read

Modern websites these days can look busy.

A lot of contents are crammed in one page.

Take a look at YouTube video page for example.

Youtube video

This page displays a lot of things:

  • Main video information such as the actual video, the channel, and the video description
  • Comments of this video
  • Recommendations of similar video

In this post we’ll discuss about:

  • How to implement this page with Next.js
  • The performance issue we will face
  • And how to fix that

Simple but slow approach

One straightforward way to render this page is by loading every data that will be displayed before rendering.

export default async function VideoDetailPage({params: {id}}) {
const video = await getVideoDetail(id)
const comments = await getVideoComments(id)
const recommendations = await getVideoRecommendations(id)
return (
<div>
<VideoPlayer url={video.url}>
<VideoDetail video={video}>
<VideoComments comments={comments}>
<VideoRecommendations recommendations={recommendations}>
</div>
)
}

When using this technique, the page content will only be rendered once all of the three requests complete.

This technique while simple, can also impose some problems:

  • It can feel slow. If one of the awaited functions take a lot of time to finish, the whole rendering process is stalled. Request waterfall In the recording below, it takes a while for the browser to load before it actually shows anything on the page. no suspense recording
  • It is fragile. If the comments or recommendations failed to load, the whole page fails to load and will display an error instead. We will discuss on how to handle this in the next post.

Faster and safer rendering with Suspense streaming

Next.js provides a way to concurrently render page contents since App Router was released.

This is what Vercel does to reduce its perceived page load time.

vercel suspense

In the recording above, there are two sections of data that are loaded and displayed independently.

vercel rendering process

Each section can render its data once loaded without having to wait for another section to finish.

How to render data in parallel

Let’s try to do the same thing with our YouTube example to improve rendering speed.

1. Split the data loading code

The first thing we need to do is to move the data loading code to each component.

VideoDetail.js
export default async function VideoDetail({id}) {
const video = await getVideoDetail(id)
return (
<div>
<VideoPlayer url={video.url}>
<VideoChannel video={video}>
<VideoDescription video={video}>
</div>
)
}
VideoComments.js
export default async function VideoComments({id}) {
const comments = await getVideoComments(id)
return (
<div>
{comments.map(comment => (
<CommentSection key={comment.id} comment={comment} />
))}
</div>
)
}
VideoRecommendations.js
export default async function VideoRecommendations({id}) {
const recommendations = await getVideoRecommendations(id)
return (
<div>
{recommendations.map(recommendation => (
<RecommendationSection key={recommendation.id} recommendation={recommendation}>
))}
</div>
)
}

If we try to put the server components as children of the page directly, the user still only sees the rendered content after all components have finished loading.

/video/[id]/page.js
export default function VideoDetail({id}) {
return (
<div>
<VideoDetail id={id}>
<VideoComments id={id}>
<VideoRecommendations id={id}>
</div>
)
}
Load data in parallel, render all at once

This is better than our initial code because the required data is loaded in parallel, but the rendering still happens at the same time after the data is loaded.

2. Render independently with Suspense

When we wrap React Server Components with Suspense, the rendering process will start independently.

Any component that has finished loading data will render its contents and stream it to the user without having to wait for anything.

/video/[id]/page.js
export default function VideoDetail({id}) {
return (
<div>
<Suspense fallback={<VideoDetailLoadingIdicator />}>
<VideoDetail id={id}>
</Suspense>
<Suspense fallback={<VideoCommentsLoadingIndicator />}>
<VideoComments id={id}>
</Suspense>
<Suspense fallback={<VideoRecommendationsLoadingIndicator />}>
<VideoRecommendations id={id}>
</Suspense>
</div>
)
}
contents rendered independently

Let’s see how the rendering looks like after we implemented Suspense. loading data with suspense

We immediately see something on the screen, and the sections are being loaded in parallel.

Any content that has finished loading is displayed immediately.