Efficient and Elegant Web Development with Next.js: A Deep Dive into Component Streaming and Chunked Transfer Encoding

Momen Daoud
10 min readNov 18, 2023
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:

  1. It needs to download 12 bytes of content (the string Hello World! comprises just 12 characters).
  2. Once downloaded, it can display this content or provide it to the callback of a fetch request.

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 http://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.

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:

  1. Initiating the Chunked Transfer: Next.js sends the Transfer-Encoding: chunked header, indicating to the browser that the response length is undetermined at this stage.
  2. Executing Home Page: As the Home page executes, it encounters no await operations. This means that no data fetching is blocking the response from being sent immediately.
  3. Handling the Suspense: Upon reaching the <Suspense /> tag, Next.js uses the fallback value for immediate rendering, while also inserting a placeholder <template /> tag. This will be used later to insert the actual HTML once it’s ready.
  4. Initial Response to the Browser: What’s been rendered so far is sent to the browser. Yet, the “0␍␊␍␊” sequence hasn’t been sent, indicating that the browser should expect more data to come.
  5. Component Data Request: The server communicates with <MyComponent />, requesting its data and essentially saying, “We need your content, let us know when you’re ready.”
  6. Component Rendering: After <MyComponent /> fetches its data, it renders and produces the corresponding HTML.
  7. Sending the Component’s HTML: This HTML is then sent to the browser as a new chunk.
  8. JavaScript Attachment: The browser’s JavaScript then appends this new chunk of HTML to the previously placed <template /> tag from step #3.
  9. Termination Sequence: Finally, the server sends the termination sequence, signaling the end of the response.”

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:

  1. Fallbacks at the Forefront: Each <Suspense /> tag comes equipped with its own fallback. During the rendering phase, all these fallback values are leveraged simultaneously, ensuring that every suspended component offers a provisional visual cue to the user. This is an extension of the third point from our previous list.
  2. Unified Request for Content: Just as with a single <Suspense />, Next.js sends out a unified call to all components wrapped within the <Suspense /> tags. It's essentially broadcasting, "Provide your content as soon as you're ready."
  3. Waiting for All Components: The termination sequence is of utmost importance, signaling the end of a response. However, in cases with multiple <Suspense /> tags, the termination sequence is held back until every single component has sent its content. This ensures that the browser knows to expect, and subsequently render, the content from all components, providing a holistic page-view to the end user.

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! 🚀

--

--

Momen Daoud

A software engineer who is passionate about understanding the concepts and fundamentals behind how things work behind the scenes.