Course
Suspense
Next.js Mastery: From Fundamentals to Full-Stack
Unlock the power of Next.js with this comprehensive course! Starting with the basics, you’ll learn essential skills such as routing, data fetching, and styling. Progress through practical projects, including building your own React Notes app, to gain hands-on experience. Dive into the source code to understand the inner workings and core principles of Next.js. Perfect for developers with a basic knowledge of React and Node.js, this course ensures you’ll be ready to create high-performance full-stack applications efficiently. Join us and master Next.js, backed by industry experts and a supportive learning community.
Introduction
Suspense is a commonly used component in Next.js projects. Understanding its principles and background helps us use the Suspense component correctly.
Traditional SSR
In recent articles, we discussed the principles and drawbacks of SSR. Simply put, using SSR requires a series of steps before users can view and interact with the page. These steps are:
- The server fetches all the data.
- The server renders HTML.
- The server sends the HTML, CSS, and JavaScript to the client.
- The client uses HTML and CSS to generate a non-interactive user interface (UI).
- React hydrates the UI, making it interactive.
These steps are sequential and blocking. This means the server can only render HTML after fetching all the data, and React can only hydrate after downloading all the component code:
Let's recall the drawbacks of SSR summarized in the last article:
- Data fetching for SSR must occur before component rendering.
- Component JavaScript must be loaded on the client before hydration can begin.
- All components must be hydrated before any interaction can occur.
Suspense
To address these issues, React 18 introduced the
<Suspense>
component. Let's introduce this component:<Suspense>
allows you to delay rendering some content until certain conditions are met (e.g., data loading is complete).You can wrap dynamic components in
Suspense
and pass a fallback UI to display while the dynamic components load. If data requests are slow, Suspense
streams the component without affecting the rest of the page, ensuring it doesn't block the entire page.Let's write an example by creating
app/dashboard/page.tsx
with the following code:import { Suspense } from 'react';
const sleep = (ms: number) => new Promise(r => setTimeout(r, ms));
async function PostFeed() { await sleep(2000); return <h1>Hello PostFeed</h1>;}
async function Weather() { await sleep(8000); return <h1>Hello Weather</h1>;}
async function Recommend() { await sleep(5000); return <h1>Hello Recommend</h1>;}
export default function Dashboard() { return ( <section style={{ padding: '20px' }}> <Suspense fallback={<p>Loading PostFeed Component</p>}> <PostFeed /> </Suspense> <Suspense fallback={<p>Loading Weather Component</p>}> <Weather /> </Suspense> <Suspense fallback={<p>Loading Recommend Component</p>}> <Recommend /> </Suspense> </section> );}
In this example, we wrapped three components in Suspense and simulated data request durations using the
sleep
function. The loading effect is as follows:
But how does Next.js achieve this?
When you observe the loading of the
dashboard
HTML file, you will see it initially takes 2.03s, then 5.03s, and finally 8.04s, exactly matching the sleep durations we set.Check the response headers for the
dashboard
request:
The
Transfer-Encoding
header is set to chunked
, indicating that data is sent in a series of chunks.Chunked transfer encoding is a data transfer mechanism in the HTTP/1.1 protocol that allows the server to send data in chunks to the client (typically a web browser).
Next, check the data returned by the
dashboard
request (simplified here):htmlCopy code<!DOCTYPE html><html lang="en"> <head> <!-- ... --> </head> <body class="__className_aaf875"> <section style="padding:20px"> <!--$?--> <template id="B:0"></template> <p>Loading PostFeed Component</p> <!--/$--> <!--$?--> <template id="B:1"></template> <p>Loading Weather Component</p> <!--/$--> <!--$?--> <template id="B:2"></template> <p>Loading Recommend Component</p> <!--/$--> </section> <!-- ... --> <div hidden id="S:0"> <h1>Hello PostFeed</h1> </div> <script> // Swap positions $RC = function(b, c, e) { // ... }; $RC("B:0", "S:0"); </script> <div hidden id="S:2"> <h1>Hello Recommend</h1> </div> <script> $RC("B:2", "S:2"); </script> <div hidden id="S:1"> <h1>Hello Weather</h1> </div> <script> $RC("B:1", "S:1"); </script> </body></html>
You can see that the fallback UI and the rendered content appear in the HTML file. This shows that the request remains connected to the server, and the server appends the rendered content to the client as components finish rendering. The client receives the new content, parses it, and executes functions like
$RC("B:2", "S:2")
to swap DOM content, replacing the fallback UI with the rendered content.This process is called Streaming Server Rendering, addressing the first problem of traditional SSR: data fetching must occur before component rendering. With Suspense, fallback UI is rendered first, and the specific component content is rendered when data is available.
Suspense also enables Selective Hydration. Simply put, when multiple components await hydration, React can prioritize hydration based on user interactions. For example, if Sidebar and MainContent components are awaiting hydration and the user clicks on the MainContent component, React will hydrate the MainContent component synchronously to ensure immediate response, delaying Sidebar hydration.
Summary
Using
Suspense
unlocks two significant benefits, enhancing SSR capabilities:- Streaming Server Rendering: Progressive HTML rendering from server to client.
- Selective Hydration: React prioritizes hydration based on user interactions.
Does Suspense Affect SEO?
First, Next.js waits for data requests in
generateMetadata
to complete before streaming the UI to the client, ensuring the first part of the response includes the <head>
tag.Secondly, because Streaming involves progressive rendering, the final HTML contains the rendered content, so it doesn't affect SEO.
Controlling Render Order with Suspense
In the previous example, we rendered three components simultaneously. However, sometimes you may want to display components in a specific order, such as showing PostFeed first, then Weather, and finally Recommend. You can achieve this by nesting Suspense components:
import { Suspense } from 'react';
const sleep = (ms: number) => new Promise(r => setTimeout(r, ms));
async function PostFeed() { await sleep(2000); return <h1>Hello PostFeed</h1>;}
async function Weather() { await sleep(8000); return <h1>Hello Weather</h1>;}
async function Recommend() { await sleep(5000); return <h1>Hello Recommend</h1>;}
export default function Dashboard() { return ( <section style={{ padding: '20px' }}> <Suspense fallback={<p>Loading PostFeed Component</p>}> <PostFeed /> <Suspense fallback={<p>Loading Weather Component</p>}> <Weather /> <Suspense fallback={<p>Loading Recommend Component</p>}> <Recommend /> </Suspense> </Suspense> </Suspense> </section> );}
Now, the question arises: what is the final load time of the page? Is it the longest request time of 8 seconds or the sum of the times, 2 + 8 + 5 = 15 seconds?
Let's see the effect:
The answer is 8 seconds. These data requests are sent simultaneously, so when the Weather component returns, the Recommend component is immediately displayed.
Note: This is because the data requests do not have a dependency on each other. If there were dependencies, it would be different.