DEV Community

Cover image for The Next.js Black Box: What Actually Happens Between Your React Code and the Browser
Pavel
Pavel

Posted on

The Next.js Black Box: What Actually Happens Between Your React Code and the Browser

Most developers think Next.js simply converts React components into HTML strings and sends them to the browser.

Then you open the "View Source" of your production App Router application, expecting clean HTML. Instead, you are greeted by thousands of lines of cryptic, unreadable JavaScript looking like this: self.__next_f.push([1, "M1:{\"id\":\"...\"}"]).

If you have ever wondered what this payload is, why it exists, and how Next.js actually compiles your codebase, you are in the right place.

Let’s open the black box and break down the modern Next.js compilation and rendering pipeline.

Phase 1: The Graph Split (AST Analysis)

The magic starts during the build step. Modern Next.js doesn't treat your application as a single tree of components. It uses an Abstract Syntax Tree (AST) analysis to split your codebase into two distinct dependency graphs:

The Server Component Graph

The Client Component Graph

Diagram illustrating the separation of Server and Client component dependency graphs

By default, everything is a Server Component. The compiler walks down the tree until it hits a file with the "use client" directive. This directive acts as a hard boundary. The compiler bundles everything below that boundary into standard JavaScript chunks that the browser will eventually download.

Everything above it stays firmly on the server, ensuring your database credentials and heavy dependencies never leak into the client bundle.

Phase 2: The Serialization Engine (Flight Protocol)

When a user requests a page, Next.js does not immediately generate standard HTML for the Server Components. First, it serializes the executed Server Components into a highly optimized, streamable format called the RSC Payload (using React's internal Flight Protocol).

This brings us back to the self.__next_f.push mystery.

Those cryptic lines are the RSC Payload being streamed inline. Next.js embeds this data directly into the HTML document so that the client-side React runtime can reconstruct the component tree without making a second network request.

Example:

M1:{"id":"./src/Counter.client.js","chunks":["client1"],"name":"Counter","async":true}

J0:["$","div",null,{"className":"main-container","children":[["$","h1",null,{"children":"Hello, React Server Components!"}],

["$","@1",null,{"initialCount":0}]]}]
Enter fullscreen mode Exit fullscreen mode

Let's dive into this mystery language:

1. The Module Definition (M1:)

  • M Prefix: Stands for Module Metadata. It tells the client that this line does not contain HTML or UI tags, but references a Client Component.
  • 1: A unique identifier for this specific client module within the stream.
  • id & chunks: Directives for the bundler (e.g., Webpack, Turbopack). It tells the browser exactly which JavaScript file (./src/Counter.client.js) and asset chunk (client1) to fetch from the server.
  • name: The specific exported component name (Counter) to instantiate.

2. The JSON UI Tree (J0:)

  • J Prefix: Stands for JSON React Element Tree.
  • 0: Represents the root ID of the tree. The client uses J0 to render the main entry point.
  • $ Symbol: This is a serialized placeholder for Symbol.for('react.element'). It signals to the React runtime that this array must be parsed as a standard React element.
  • "div": The HTML tag type.
  • null: Reserved for the element's key prop (if none is provided).
  • The Object ({...}): Contains the component's props (className, children, etc.).

3. The Client Component Reference

  • @1: The @ symbol indicates a Reference Link. This tells React to stop reading raw JSON and instead look up the Module defined earlier under ID 1 (the Counter component).
  • {"initialCount":0}: These are the props passed from the Server Component down to the Client Component.

Phase 3: Assembly and Partial Prerendering (PPR)

Ilustration of basic web page with shells and holes

In the latest iterations of Next.js, this pipeline has evolved to support Partial Prerendering (PPR). The compiler orchestrates the rendering using a "Static Shell + Dynamic Holes" model.

Here is how the compiler handles a page request:

  • The Static Shell: During the build, the compiler pre-renders static UI (headers, footers, and Suspense fallback skeletons) and caches it at the edge.

  • The Dynamic Holes: When the compiler encounters a dynamic function (like cookies(), headers(), or database calls wrapped in a Suspense boundary), it pauses execution for that subtree.

Diagram illustrating the life-time of shells and holes

When a request comes in, the server instantly flushes the cached static HTML shell to the browser, achieving an incredibly low Time to First Byte (TTFB). Meanwhile, it resumes executing the dynamic "holes" in the background and streams the final RSC payload down the open HTTP connection as soon as it resolves.

The Takeaway

Next.js is no longer just a traditional SSR framework. It is a sophisticated compiler that splits your code, serializes Server Components into a custom streaming protocol, and interleaves static and dynamic content on the fly.

Understanding this pipeline is the key to optimizing your app's performance. The next time you see self.__next_f.push, you’ll know exactly what the engine is doing under the hood.

Have you ever run into payload bloat issues with massive RSC payloads on data-heavy pages? How did you solve it? Let's discuss in the comments! 👇

Top comments (0)