Efficient and Elegant Web Development with Next.js: A Deep Dive into Component Streaming and Chunked Transfer?Encoding
Introduction
Welcome to my article on Next.js and its features! In this article, we’ll take a deep dive into component streaming and chunked transfer encoding. We’ll explore how Next.js leverages these technologies to optimize content delivery and enhance user experience. We’ll also examine the nuances of HTTP transmission and how Next.js aligns with the realities of web browsing. By the end of this article, you’ll have a better understanding of how to use Next.js to create efficient and elegant web applications. Let’s get started! ??
Content Overview
1- What is streaming?
2- <Suspense />
3- Diving into Multiple <Suspense />
What is streaming?
Before we explore “Components Streaming” it’s important to understand the concept of streaming itself. When your browser sends an HTTP request to a server, the server replies with something like:
HTTP/1.1 200 OK??
Date: Sat, 18 Nov 2023 12:28:53 GMT??
Content-Length: 12??
Content-Type: text/plain??
??
Hello World!
The first line of the server’s response, HTTP/1.1 200 OK, indicates that the server has responded with a 200 OK code, which means that everything is fine. After this, we have three lines known as headers. In our example, these headers are Date, Content-Length, and Content-Type. We can think of them as key-value pairs, where the keys and values are separated by a colon (:).
After the headers, there is an empty line that separates the header and body sections. The content itself follows this line. Based on the information from the headers, our browser can understand two things:
In other words, we can conclude that the response body ends after reading 12 bytes following a new line.
What happens if we don’t include the Content-Length header in our server response? In this case, many HTTP servers will automatically add a Transfer-Encoding: chunked header. This type of response can be interpreted as, “Hello, I’m the server, and I’m not sure how much content there will be, so I’ll send the data in chunks.”
HTTP/1.1 200 OK??
Date: Sat, 18 Nov 2023 12:28:53 GMT??
Transfer-Encoding: chunked??
Content-Type: text/plain??
??
5??
Hello??
At this point, we have only received the first 5 bytes of the message. It’s worth noting that the format of the body differs from the headers. First, the size of the chunk is sent, followed by the content of the chunk itself. At the end of each chunk, the server adds a ?? sequence.
Now, let’s consider receiving the second chunk.
How might that appear?
HTTP/1.1 200 OK??
Date: Sat, 18 Nov 2023 12:28:53 GMT??
Transfer-Encoding: chunked??
Content-Type: text/plain??
??
5??
Hello??
7??
World!??
We’ve received an additional 7 bytes of the response. But what happened between Hello?? and 7??? How was the response processed during this interval? Imagine that before sending the 7, the server took 10 seconds to ponder the next word. If you were to inspect the Network tab of your browser’s Developer Tools during this pause, you would see that the response from the server had started and remained “in progress” throughout these 10 seconds. This is because the server had not indicated the end of the response.
So, how does the browser determine when the response should be treated as “completed”? There’s a convention for that. The server must send a 0???? sequence. In simpler terms, it’s saying, “I’m sending you a chunk that has zero length, signifying that there’s nothing more to come.” In the Network tab, this sequence will mark the moment the request has concluded.
HTTP/1.1 200 OK??
Date: Sat, 18 Nov 2023 12:28:53 GMT??
Transfer-Encoding: chunked??
Content-Type: text/plain??
??
5??
Hello??
7??
World!??
0??
??
Understanding HTTP Transmission
When it comes to HTTP headers, it’s important to understand the difference between Content-Length: <number> and Transfer-Encoding: chunked. At first glance, Content-Length: <number> might suggest that data isn’t streamed, but this isn’t entirely accurate. While this header indicates the total length of the data to be received, it doesn’t imply that data is transmitted as a single massive chunk. Underneath the HTTP layer, protocols like TCP/IP dictate the actual transmission mechanics, which inherently involve breaking data down into smaller packets.
So, while the Content-Length header signals that the system is ready for rendering once it accumulates the specified amount of data, the actual data transfer is executed incrementally at a lower level. Some contemporary browsers capitalize on this inherent packetization and initiate the rendering process even before the entire data is received. This is particularly beneficial for specific data formats that lend themselves to progressive rendering. On the other hand, the Transfer-Encoding: chunked header offers more explicit control over data streaming at the HTTP level, marking each chunk of data as it’s sent. This provides even more flexibility, especially for dynamically generated content or when the full content length is unknown at the outset.
<Suspense />
Alright, now that we’ve covered a foundational concept that’s crucial for Component Streaming in Next.js, let’s first define the problem it addresses before diving into <Suspense />. Sometimes, it’s more instructive to see something in action than to read a lengthy explanation.
So, let’s create a helper function for illustration:
export function wait<T>(ms: number, data: T) {
return new Promise<T>((resolve) => {
setTimeout(() => resolve(data), ms);
});
}
This function helps us simulate long, fake requests.
To start, initialize a Next.js app using npx create-next-app@latest.
Clear out any unnecessary elements, and paste the following code into app/page.tsx:
领英推荐
import { wait } from "@/helpers/wait";
const MyComponent = async () => {
const data = await wait(10000, { name: "Momen" });
return <p>{data.name}</p>;
};
export const dynamic = "force-dynamic";
export default async function Home() {
return (
<>
<p>Some text</p>
<MyComponent />
</>
);
This structure consists of a text block containing “Some text” and a component that waits for 10 seconds before outputting the data.
To see this in action, execute npm run build && npm run start then open https://localhost:3000 in your browser.
What happens next?
You’ll experience a delay of 10 seconds before receiving the entire page content, including both “Some text” and “Momen”. This means that users won’t be able to view the “Some text” content while <MyComponent /> is fetching its data. This is far from ideal; the browser tab’s spinner will keep spinning for a solid 10 seconds before displaying any content to the user.
However, by wrapping our component with the <Suspense/> tag and trying again, we observe an instantaneous response. Let's delve into this method.
We encase our component in <Suspense> and also assign a fallback prop with the value "We are loading...".
export default async function Home() {
return (
<>
<p>Some text</p>
<Suspense fallback={"We are loading..."}>
<MyComponent />
</Suspense>
</>
);
}
Now let us open it in a browser.
Now, we observe that the string provided as the fallback prop for <Suspense /> temporarily stands in for the <MyComponent />. After the 10-second wait, we're then presented with the actual content.
Let’s examine the HTML response we received.
<!DOCTYPE html>
<html lang="en">
<head>
<!-- Omitted -->
</head>
<body class="__className_20951f">
<p>Some text</p><!--$?-->
<template id="B:0"></template>
Waiting for MyComponent...<!--/$-->
<script src="/_next/static/chunks/webpack-f0069ae2f14f3de1.js" async=""></script>
<script>(self.__next_f = self.__next_f || []).push([0])</script>
<script>self.__next_f.push(/* Omitted */)</script>
<script>self.__next_f.push(/* Omitted */)</script>
<script>self.__next_f.push(/* Omitted */)</script>
<script>self.__next_f.push(/* We haven't received a chunk that closes this tag...
While we haven’t yet received the complete page, we can already view its content in the browser. But why is that possible? This behavior is due to the error tolerance of modern browsers. Consider a scenario where you visit a website, but because a developer forgot to close a tag, the site doesn’t display correctly. Although browser developers could enforce strict error-free HTML, such a decision would degrade the user experience. As users, we expect web pages to load and display their content, regardless of minor errors in the underlying code. To ensure this, browsers implement numerous mechanisms under the hood to compensate for such issues. For instance, if there’s an opened <body> tag that hasn't been closed, the browser will automatically "close" it. This is done in an effort to deliver the best possible viewing experience, even when faced with imperfect HTML.
And it’s evident that Next capitalizes on this inherent browser behavior when implementing Component Streaming. By pushing chunks of content as they become available and leveraging browsers’ ability to interpret and render partial or even slightly malformed content, Next.js ensures faster-perceived load times and enhances user experience.
The strength of this approach lies in its alignment with the realities of web browsing. Users generally prefer immediate feedback, even if it’s incremental, over waiting for an entire page to load. By sending parts of a page as soon as they’re ready, Next.js optimally meets this preference.
Now, observe this segment:
<!--$?-->
<template id="B:0"></template>
Waiting for MyComponent...
<!--/$-->
We can spot our placeholder text adjacent to an empty <template> tag bearing the B:0 id. Further, we can discern that the response from localhost:3000 is still underway. The trailing script tag remains unclosed. Next.js uses a placeholder template to make room for forthcoming HTML that will be populated with the next chunk.
After the next chunk has arrived, we now have the following markup (I’ve added some newlines to make it more readable)…
Don’t attempt to un minify the code of the $RC function in your head. This is the completeBoundary function, and you can find a commented version here.
<p>Some text</p>
<!--$?-->
<template id="B:0"></template>
Waiting for MyComponent...
<!--/$-->
<!-- <script> tags omitted -->
<div hidden id="S:0">
<p>Momen</p>
</div>
<script>
$RC = function (b, c, e) {
c = document.getElementById(c);
c.parentNode.removeChild(c);
var a = document.getElementById(b);
if (a) {
b = a.previousSibling;
if (e)
b.data = "$!",
a.setAttribute("data-dgst", e);
else {
e = b.parentNode;
a = b.nextSibling;
var f = 0;
do {
if (a && 8 === a.nodeType) {
var d = a.data;
if ("/$" === d)
if (0 === f)
break;
else
f--;
else
"$" !== d && "$?" !== d && "$!" !== d || f++
}
d = a.nextSibling;
e.removeChild(a);
a = d
} while (a);
for (; c.firstChild;)
e.insertBefore(c.firstChild, a);
b.data = "$"
}
b._reactRetry && b._reactRetry()
}
}
;
$RC("B:0", "S:0")
</script>
We receive a hidden <div> with the id="S:0". This contains the markup for <MyComponent />. Alongside this, we are presented with an intriguing script that defines a global variable, $RC. This variable references a function that performs some operations with getElementById and insertBefore.
The concluding statement in the script, $RC("B:0", "S:0"), invokes the aforementioned function and supplies "B:0" and "S:0" as arguments. As we've deduced, B:0 corresponds to the ID of the template that previously held our fallback. Concurrently, S:0 matches the ID of the newly acquired <div>. To distill this information, the $RC function essentially instructs: "Retrieve the markup from the S:0 div and position it where the B:0 template resides."
Here’s a possible revision of the paragraph:
Let’s break this down for clarity:
Diving into Multiple <Suspense />
Handling a single <Suspense /> tag is straightforward, but what if a page has multiple tags? How does Next.js cope with this situation? Interestingly, the core approach doesn’t deviate much. Here’s what changes when managing multiple <Suspense /> tags:
And that’s it! We hope you enjoyed this deep dive into component streaming and chunked transfer encoding. By tapping into the innate behavior of browsers and optimizing content delivery, Next.js ensures users encounter minimal wait times and see content as swiftly as possible. As web developers, understanding these nuances not only makes us better at our craft but also equips us to deliver seamless and responsive digital experiences for our users. So why not give Next.js a try and see how it can help you create efficient and elegant web applications? Happy coding! ??