<script>self.__next_f.push([1,"3:I[\"(app-pages-browser)/./node_modules/next/dist/client/components/app-router.js\",[\"app-pages-internals\",\"static/chunks/app-pages-internals.js\"],\"\"]\n5:I[\"(app-pages-browser)/./node_modules/next/dist/client/components/client-page.js\",[\"app-pages-internals\",\"static/chunks/app-pages-internals.js\"],\"ClientPageRoot\"]\n6:I[\"(app-pages-browser)/./app/flight/page.tsx\",[\"app/flight/page\",\"static/chunks/app/flight/page.js\"],\"default\"]\n7:I[\"(app-pages-browser)/./node_modules/next/dist/client/components/layout-router.js\",[\"app-pages-internals\",\"static/chunks/app-pages-internals.js\"],\"\"]\n8:I[\"(app-pages-browser)/./node_modules/next/dist/client/components/render-from-template-context.js\",[\"app-pages-internals\",\"static/chunks/app-pages-internals.js\"],\"\"]\nc:I[\"(app-pages-browser)/./node_modules/next/dist/client/components/error-boundary.js\",[\"app-pages-internals\",\"static/chunks/app-pages-internals.js\"],\"\"]\n4:D{\"name\":\"\",\"env\":\"Server\"}\n9:D{\"name\":\"RootLayout\",\"env\":\"Server\"}\na:D{\"name\":\"NotFound\",\"env\":\"Server\"}\na:[[\"$\",\"title\",null,{\"children\":\"404: This page could not be found.\"}],[\"$\",\"div\",null,{\"style\":{\"fontFamily\":\"system-ui,\\\"Segoe UI\\\",Roboto,Helvetica,Arial,sans-serif,\\\"Apple Color Emoji\\\",\\\"Segoe UI Emoji\\\"\",\"height\":\"100vh\",\"textAlign\":\"center\",\"display\":\"flex\",\"flexDirection\":\"column\",\"alignItems\":\"center\",\"justifyContent\":\"center\"},\"children\":[\"$\",\"div\",null,{\"children\":[[\"$\",\"style\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}\"}}],[\"$\",\"h1\",null,{\"className\":\"next-error-h1\",\"style\":{\"display\":\"inline-block\",\"margin\":\"0 20px 0 0\",\"padding\":\"0 23px 0 0\",\"fontSize\":24,\"fontWeight\":500,\"verticalAlign\":\"top\",\"lineHeight\":\"49px\"},\"children\":\"404\"}],[\"$\",\"div\",null,{\"style\":{\"display\":\"inline-block\"},\"children\":[\"$\",\"h2\",null,{\"style\":{\"fontSize\":14,\"fontWeight\":400,\"lineHeight\":\"49px\",\"margin\":0},\"childr"])</script>
<script>self.__next_f.push([1,"en\":\"This page could not be found.\"}]}]]}]}]]\n9:[\"$\",\"html\",null,{\"lang\":\"en\",\"children\":[\"$\",\"body\",null,{\"className\":\"__className_aaf875\",\"children\":[\"$\",\"$L7\",null,{\"parallelRouterKey\":\"children\",\"segmentPath\":[\"children\"],\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L8\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":\"$a\",\"notFoundStyles\":[],\"styles\":null}]}]}]\nb:D{\"name\":\"\",\"env\":\"Server\"}\nd:[]\n0:[[[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/css/app/layout.css?v=1719846361489\",\"precedence\":\"next_static/css/app/layout.css\",\"crossOrigin\":\"$undefined\"}]],[\"$\",\"$L3\",null,{\"buildId\":\"development\",\"assetPrefix\":\"\",\"initialCanonicalUrl\":\"/flight\",\"initialTree\":[\"\",{\"children\":[\"flight\",{\"children\":[\"__PAGE__\",{}]}]},\"$undefined\",\"$undefined\",true],\"initialSeedData\":[\"\",{\"children\":[\"flight\",{\"children\":[\"__PAGE__\",{},[[\"$L4\",[\"$\",\"$L5\",null,{\"props\":{\"params\":{},\"searchParams\":{}},\"Component\":\"$6\"}]],null],null]},[\"$\",\"$L7\",null,{\"parallelRouterKey\":\"children\",\"segmentPath\":[\"children\",\"flight\",\"children\"],\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L8\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":\"$undefined\",\"notFoundStyles\":\"$undefined\",\"styles\":null}],null]},[\"$9\",null],null],\"couldBeIntercepted\":false,\"initialHead\":[false,\"$Lb\"],\"globalErrorComponent\":\"$c\",\"missingSlots\":\"$Wd\"}]]\n"])</script>
<script>self.__next_f.push([1,"b:[[\"$\",\"meta\",\"0\",{\"name\":\"viewport\",\"content\":\"width=device-width, initial-scale=1\"}],[\"$\",\"meta\",\"1\",{\"charSet\":\"utf-8\"}],[\"$\",\"title\",\"2\",{\"children\":\"React Server Components Payloads\"}],[\"$\",\"meta\",\"3\",{\"name\":\"description\",\"content\":\"By Ed Spencer - edspencer.net\"}],[\"$\",\"link\",\"4\",{\"rel\":\"icon\",\"href\":\"/favicon.ico\",\"type\":\"image/x-icon\",\"sizes\":\"16x16\"}],[\"$\",\"meta\",\"5\",{\"name\":\"next-size-adjust\"}]]\n4:null\n"])</script>
<script>self.__next_f.push([1,"3:I[\"(app-pages-browser)/./node_modules/next/dist/client/components/app-router.js\",[\"app-pages-internals\",\"static/chunks/app-pages-internals.js\"],\"\"]\n5:I[\"(app-pages-browser)/./node_modules/next/dist/client/components/client-page.js\",[\"app-pages-internals\",\"static/chunks/app-pages-internals.js\"],\"ClientPageRoot\"]\n6:I[\"(app-pages-browser)/./app/flight/page.tsx\",[\"app/flight/page\",\"static/chunks/app/flight/page.js\"],\"default\"]\n7:I[\"(app-pages-browser)/./node_modules/next/dist/client/components/layout-router.js\",[\"app-pages-internals\",\"static/chunks/app-pages-internals.js\"],\"\"]\n8:I[\"(app-pages-browser)/./node_modules/next/dist/client/components/render-from-template-context.js\",[\"app-pages-internals\",\"static/chunks/app-pages-internals.js\"],\"\"]\nc:I[\"(app-pages-browser)/./node_modules/next/dist/client/components/error-boundary.js\",[\"app-pages-internals\",\"static/chunks/app-pages-internals.js\"],\"\"]\n4:D{\"name\":\"\",\"env\":\"Server\"}\n9:D{\"name\":\"RootLayout\",\"env\":\"Server\"}\na:D{\"name\":\"NotFound\",\"env\":\"Server\"}\na:[[\"$\",\"title\",null,{\"children\":\"404: This page could not be found.\"}],[\"$\",\"div\",null,{\"style\":{\"fontFamily\":\"system-ui,\\\"Segoe UI\\\",Roboto,Helvetica,Arial,sans-serif,\\\"Apple Color Emoji\\\",\\\"Segoe UI Emoji\\\"\",\"height\":\"100vh\",\"textAlign\":\"center\",\"display\":\"flex\",\"flexDirection\":\"column\",\"alignItems\":\"center\",\"justifyContent\":\"center\"},\"children\":[\"$\",\"div\",null,{\"children\":[[\"$\",\"style\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}\"}}],[\"$\",\"h1\",null,{\"className\":\"next-error-h1\",\"style\":{\"display\":\"inline-block\",\"margin\":\"0 20px 0 0\",\"padding\":\"0 23px 0 0\",\"fontSize\":24,\"fontWeight\":500,\"verticalAlign\":\"top\",\"lineHeight\":\"49px\"},\"children\":\"404\"}],[\"$\",\"div\",null,{\"style\":{\"display\":\"inline-block\"},\"children\":[\"$\",\"h2\",null,{\"style\":{\"fontSize\":14,\"fontWeight\":400,\"lineHeight\":\"49px\",\"margin\":0},\"childr"])</script>
<script>self.__next_f.push([1,"en\":\"This page could not be found.\"}]}]]}]}]]\n9:[\"$\",\"html\",null,{\"lang\":\"en\",\"children\":[\"$\",\"body\",null,{\"className\":\"__className_aaf875\",\"children\":[\"$\",\"$L7\",null,{\"parallelRouterKey\":\"children\",\"segmentPath\":[\"children\"],\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L8\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":\"$a\",\"notFoundStyles\":[],\"styles\":null}]}]}]\nb:D{\"name\":\"\",\"env\":\"Server\"}\nd:[]\n0:[[[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/css/app/layout.css?v=1719846361489\",\"precedence\":\"next_static/css/app/layout.css\",\"crossOrigin\":\"$undefined\"}]],[\"$\",\"$L3\",null,{\"buildId\":\"development\",\"assetPrefix\":\"\",\"initialCanonicalUrl\":\"/flight\",\"initialTree\":[\"\",{\"children\":[\"flight\",{\"children\":[\"__PAGE__\",{}]}]},\"$undefined\",\"$undefined\",true],\"initialSeedData\":[\"\",{\"children\":[\"flight\",{\"children\":[\"__PAGE__\",{},[[\"$L4\",[\"$\",\"$L5\",null,{\"props\":{\"params\":{},\"searchParams\":{}},\"Component\":\"$6\"}]],null],null]},[\"$\",\"$L7\",null,{\"parallelRouterKey\":\"children\",\"segmentPath\":[\"children\",\"flight\",\"children\"],\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L8\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":\"$undefined\",\"notFoundStyles\":\"$undefined\",\"styles\":null}],null]},[\"$9\",null],null],\"couldBeIntercepted\":false,\"initialHead\":[false,\"$Lb\"],\"globalErrorComponent\":\"$c\",\"missingSlots\":\"$Wd\"}]]\n"])</script>
<script>self.__next_f.push([1,"b:[[\"$\",\"meta\",\"0\",{\"name\":\"viewport\",\"content\":\"width=device-width, initial-scale=1\"}],[\"$\",\"meta\",\"1\",{\"charSet\":\"utf-8\"}],[\"$\",\"title\",\"2\",{\"children\":\"React Server Components Payloads\"}],[\"$\",\"meta\",\"3\",{\"name\":\"description\",\"content\":\"By Ed Spencer - edspencer.net\"}],[\"$\",\"link\",\"4\",{\"rel\":\"icon\",\"href\":\"/favicon.ico\",\"type\":\"image/x-icon\",\"sizes\":\"16x16\"}],[\"$\",\"meta\",\"5\",{\"name\":\"next-size-adjust\"}]]\n4:null\n"])</script>
You may be wondering what this all means. It's not super well documented, and all pretty bleeding-edge. It's not likely to be something you need to worry about in your day-to-day work, but if you're a curious geek like me, read on.
What you're looking at is a bunch of <script> tags automatically injected into the end of the page. The content above is a copy-paste from just about the most basic Next JS application imaginable. It consists of 2 components - a layout.tsx and a page.tsx:
layout.tsx
import type { Metadata } from "next";
import "./globals.css";
export const metadata: Metadata = {
title: "React Server Components Payloads",
description: "By Ed Spencer - edspencer.net",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
layout.tsx
import type { Metadata } from "next";
import "./globals.css";
export const metadata: Metadata = {
title: "React Server Components Payloads",
description: "By Ed Spencer - edspencer.net",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
page.tsx
export default function Home() {
return (
<div>
<h1>Server Component</h1>
</div>
);
}
page.tsx
export default function Home() {
return (
<div>
<h1>Server Component</h1>
</div>
);
}
In theory, this content could be rendered into the following HTML document - 236 bytes including whitespace:
theoretical-output.html
<html lang="en">
<head>
<title>React Server Component Payloads</title>
<meta name="description" content="By Ed Spencer - edspencer.net">
</head>
<body>
<div>
<h1>Server Component</h1>
</div>
</body>
</html>
theoretical-output.html
<html lang="en">
<head>
<title>React Server Component Payloads</title>
<meta name="description" content="By Ed Spencer - edspencer.net">
</head>
<body>
<div>
<h1>Server Component</h1>
</div>
</body>
</html>
Instead, we get about 5kb of stuff, mostly in the form of self.__next_f.push([1, ...]) calls. These calls are pushing payloads into an array that Next.js uses to fetch resources and hydrate the page. The payloads are in a custom format that Next.js uses to communicate between the server and the client, called the RSC Payload.
React Server Component Payloads
I spent a few hours digging through the React and Next.js source code to figure out what these payloads are and how they work. Instructive in this effort were the following source code files:
Within next/src/client/app-index.tsx we can see that the self.__next_f array is being created, and its push function overridden to call nextServerDataCallback each time __next_f.push(...) is called. Each push to that array is expected to be in the form of a 2-tuple, where the first element is a number between 0 and 3, and the second is some string of data (or undefined if the first element was 0).
Most of the time you're going to see most of the payloads being of type INLINE_FLIGHT_PAYLOAD_DATA (1), which can contain a variety of different types of content.
Basic format
The way the payload gets decoded depends on the value of the first element in the array. Let's focus on calls that send INLINE_FLIGHT_PAYLOAD_DATA (1) payloads, as they're the most common and the most interesting. Each payload item contains one or more rows, each of which contains the following parts:
ROW_ID: a unique identifier for the payload
ROW_TAG: a string that identifies the type of payload
ROW_DATA: the actual payload data
NEW_LINE: a newline character (\n) indicates the end of the row
Here we see that this is a payload of type 1, and the data is a string that contains 3 rows. Each of these rows is separated by a newline character. The first row is a font, the second is a style, and the third is a data row. The data row is a JSON object with a name and env property.
Let's rearrange that to make it a bit easier to understand:
Ok that's a bit more interesting. So we actually got 3 rows of data in that first <script> tag. Two of them are of tag type HL, which are "hints" and are ultimately turned into various type of <link> tags and similar to load CSS, fonts, JS and other resources. The third is a data row, which is used to pass data from the server to the client. The row IDs are not in order, but that doesn't seem to matter very much.
2kb at a time
A wrinkle here is that the payloads are limited to 2kb in size. If you try to push a payload larger than that, Next.js will split it into multiple payloads. We see this in the next calls to __next_f.push, which look like this:
self.__next_f.push([1,"3:I[\"(app-pages-browser)/./node_modules/next/dist/client/components/app-router.js\",[\"app-pages-internals\",\"static/chunks/app-pages-internals.js\"],\"\"]\n5:I[\"(app-pages-browser)/./node_modules/next/dist/client/components/client-page.js\",[\"app-pages-internals\",\"static/chunks/app-pages-internals.js\"],\"ClientPageRoot\"]\n6:I[\"(app-pages-browser)/./app/flight/page.tsx\",[\"app/flight/page\",\"static/chunks/app/flight/page.js\"],\"default\"]\n7:I[\"(app-pages-browser)/./node_modules/next/dist/client/components/layout-router.js\",[\"app-pages-internals\",\"static/chunks/app-pages-internals.js\"],\"\"]\n8:I[\"(app-pages-browser)/./node_modules/next/dist/client/components/render-from-template-context.js\",[\"app-pages-internals\",\"static/chunks/app-pages-internals.js\"],\"\"]\nc:I[\"(app-pages-browser)/./node_modules/next/dist/client/components/error-boundary.js\",[\"app-pages-internals\",\"static/chunks/app-pages-internals.js\"],\"\"]\n4:D{\"name\":\"\",\"env\":\"Server\"}\n9:D{\"name\":\"RootLayout\",\"env\":\"Server\"}\na:D{\"name\":\"NotFound\",\"env\":\"Server\"}\na:[[\"$\",\"title\",null,{\"children\":\"404: This page could not be found.\"}],[\"$\",\"div\",null,{\"style\":{\"fontFamily\":\"system-ui,\\\"Segoe UI\\\",Roboto,Helvetica,Arial,sans-serif,\\\"Apple Color Emoji\\\",\\\"Segoe UI Emoji\\\"\",\"height\":\"100vh\",\"textAlign\":\"center\",\"display\":\"flex\",\"flexDirection\":\"column\",\"alignItems\":\"center\",\"justifyContent\":\"center\"},\"children\":[\"$\",\"div\",null,{\"children\":[[\"$\",\"style\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}\"}}],[\"$\",\"h1\",null,{\"className\":\"next-error-h1\",\"style\":{\"display\":\"inline-block\",\"margin\":\"0 20px 0 0\",\"padding\":\"0 23px 0 0\",\"fontSize\":24,\"fontWeight\":500,\"verticalAlign\":\"top\",\"lineHeight\":\"49px\"},\"children\":\"404\"}],[\"$\",\"div\",null,{\"style\":{\"display\":\"inline-block\"},\"children\":[\"$\",\"h2\",null,{\"style\":{\"fontSize\":14,\"fontWeight\":400,\"lineHeight\":\"49px\",\"margin\":0},\"childr"])
self.__next_f.push([1,"en\":\"This page could not be found.\"}]}]]}]}]]\n9:[\"$\",\"html\",null,{\"lang\":\"en\",\"children\":[\"$\",\"body\",null,{\"className\":\"__className_aaf875\",\"children\":[\"$\",\"$L7\",null,{\"parallelRouterKey\":\"children\",\"segmentPath\":[\"children\"],\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L8\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":\"$a\",\"notFoundStyles\":[],\"styles\":null}]}]}]\nb:D{\"name\":\"\",\"env\":\"Server\"}\nd:[]\n0:[[[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/css/app/layout.css?v=1719846361489\",\"precedence\":\"next_static/css/app/layout.css\",\"crossOrigin\":\"$undefined\"}]],[\"$\",\"$L3\",null,{\"buildId\":\"development\",\"assetPrefix\":\"\",\"initialCanonicalUrl\":\"/flight\",\"initialTree\":[\"\",{\"children\":[\"flight\",{\"children\":[\"__PAGE__\",{}]}]},\"$undefined\",\"$undefined\",true],\"initialSeedData\":[\"\",{\"children\":[\"flight\",{\"children\":[\"__PAGE__\",{},[[\"$L4\",[\"$\",\"$L5\",null,{\"props\":{\"params\":{},\"searchParams\":{}},\"Component\":\"$6\"}]],null],null]},[\"$\",\"$L7\",null,{\"parallelRouterKey\":\"children\",\"segmentPath\":[\"children\",\"flight\",\"children\"],\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L8\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":\"$undefined\",\"notFoundStyles\":\"$undefined\",\"styles\":null}],null]},[\"$9\",null],null],\"couldBeIntercepted\":false,\"initialHead\":[false,\"$Lb\"],\"globalErrorComponent\":\"$c\",\"missingSlots\":\"$Wd\"}]]\n"])
self.__next_f.push([1,"3:I[\"(app-pages-browser)/./node_modules/next/dist/client/components/app-router.js\",[\"app-pages-internals\",\"static/chunks/app-pages-internals.js\"],\"\"]\n5:I[\"(app-pages-browser)/./node_modules/next/dist/client/components/client-page.js\",[\"app-pages-internals\",\"static/chunks/app-pages-internals.js\"],\"ClientPageRoot\"]\n6:I[\"(app-pages-browser)/./app/flight/page.tsx\",[\"app/flight/page\",\"static/chunks/app/flight/page.js\"],\"default\"]\n7:I[\"(app-pages-browser)/./node_modules/next/dist/client/components/layout-router.js\",[\"app-pages-internals\",\"static/chunks/app-pages-internals.js\"],\"\"]\n8:I[\"(app-pages-browser)/./node_modules/next/dist/client/components/render-from-template-context.js\",[\"app-pages-internals\",\"static/chunks/app-pages-internals.js\"],\"\"]\nc:I[\"(app-pages-browser)/./node_modules/next/dist/client/components/error-boundary.js\",[\"app-pages-internals\",\"static/chunks/app-pages-internals.js\"],\"\"]\n4:D{\"name\":\"\",\"env\":\"Server\"}\n9:D{\"name\":\"RootLayout\",\"env\":\"Server\"}\na:D{\"name\":\"NotFound\",\"env\":\"Server\"}\na:[[\"$\",\"title\",null,{\"children\":\"404: This page could not be found.\"}],[\"$\",\"div\",null,{\"style\":{\"fontFamily\":\"system-ui,\\\"Segoe UI\\\",Roboto,Helvetica,Arial,sans-serif,\\\"Apple Color Emoji\\\",\\\"Segoe UI Emoji\\\"\",\"height\":\"100vh\",\"textAlign\":\"center\",\"display\":\"flex\",\"flexDirection\":\"column\",\"alignItems\":\"center\",\"justifyContent\":\"center\"},\"children\":[\"$\",\"div\",null,{\"children\":[[\"$\",\"style\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}\"}}],[\"$\",\"h1\",null,{\"className\":\"next-error-h1\",\"style\":{\"display\":\"inline-block\",\"margin\":\"0 20px 0 0\",\"padding\":\"0 23px 0 0\",\"fontSize\":24,\"fontWeight\":500,\"verticalAlign\":\"top\",\"lineHeight\":\"49px\"},\"children\":\"404\"}],[\"$\",\"div\",null,{\"style\":{\"display\":\"inline-block\"},\"children\":[\"$\",\"h2\",null,{\"style\":{\"fontSize\":14,\"fontWeight\":400,\"lineHeight\":\"49px\",\"margin\":0},\"childr"])
self.__next_f.push([1,"en\":\"This page could not be found.\"}]}]]}]}]]\n9:[\"$\",\"html\",null,{\"lang\":\"en\",\"children\":[\"$\",\"body\",null,{\"className\":\"__className_aaf875\",\"children\":[\"$\",\"$L7\",null,{\"parallelRouterKey\":\"children\",\"segmentPath\":[\"children\"],\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L8\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":\"$a\",\"notFoundStyles\":[],\"styles\":null}]}]}]\nb:D{\"name\":\"\",\"env\":\"Server\"}\nd:[]\n0:[[[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/css/app/layout.css?v=1719846361489\",\"precedence\":\"next_static/css/app/layout.css\",\"crossOrigin\":\"$undefined\"}]],[\"$\",\"$L3\",null,{\"buildId\":\"development\",\"assetPrefix\":\"\",\"initialCanonicalUrl\":\"/flight\",\"initialTree\":[\"\",{\"children\":[\"flight\",{\"children\":[\"__PAGE__\",{}]}]},\"$undefined\",\"$undefined\",true],\"initialSeedData\":[\"\",{\"children\":[\"flight\",{\"children\":[\"__PAGE__\",{},[[\"$L4\",[\"$\",\"$L5\",null,{\"props\":{\"params\":{},\"searchParams\":{}},\"Component\":\"$6\"}]],null],null]},[\"$\",\"$L7\",null,{\"parallelRouterKey\":\"children\",\"segmentPath\":[\"children\",\"flight\",\"children\"],\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L8\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":\"$undefined\",\"notFoundStyles\":\"$undefined\",\"styles\":null}],null]},[\"$9\",null],null],\"couldBeIntercepted\":false,\"initialHead\":[false,\"$Lb\"],\"globalErrorComponent\":\"$c\",\"missingSlots\":\"$Wd\"}]]\n"])
What we're looking at here are 14 rows of data, sent as a single payload but split into two chunks. Whereas in the first example we looked at the payload ended with a \n, here it ends with a ]. This is because the payload is split into two parts, and the second part is sent as a separate payload. If you're not into horizontal scrolling, the end of that first line above is:
\":0},\"childr"])
\":0},\"childr"])
And the start of the second line is:
self.__next_f.push([1,"en\":\"This page could not be found.\"}]}]]}]}]]\n
self.__next_f.push([1,"en\":\"This page could not be found.\"}]}]]}]}]]\n
See that the string content of that second one starts with "en", completing the childr at the end of the first. It's pretty obvious by looking at this that the word children is being split across these two <script> tags. If we pull up the browser console and look at the length of the first chunk (the one ending in \"childr"]), we see that it's 2048 characters long, which is a pretty notable number.
So if we were to parse the whole payload into our JSON format, we'd end up with something like this:
"ROW_DATA": [["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":[["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)"}}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]]
"ROW_DATA": [["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":[["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)"}}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]]
There are a few interesting things going on here. Most of the rows are of type I, which are "imports". These are used to load the various JavaScript components that the page needs. Some of the row IDs are duplicated ("a" appears twice, for example). The rows are out of order, but again this doesn't seem to matter. There are quite a few duplicated strings, but gzip will do a good job with those.
It looks like a default 404 React page is inlined in the response here, which is interesting. Finally, the whole thing is sent in the same HTTP response as the page, so no extra HTTP requests are needed to fetch this data (though of course we will still make a few requests for the resources defined in those I blocks).
Opaque? Yeah kinda, but it is just about scrutable if you have time to wade through the various source files. Let's look at a couple more examples.
Using Suspense
Let's swap out the page.tsx file for one that uses Suspense. Note that this is an async function (more on that later):
Looking at the source, we can see that this first row is going to resolve to a Symbol.for lookup for the react.suspense symbol. I didn't trace this through any further but I would bet this is telling Next.js that we're using Suspense and to load the appropriate resources.
Row ID 5 then defines a specification for a React element that will be rendered on the client. It's a Suspense component with a fallback of a div that says "Loading..." and a child of "resolved data". This is the data that was resolved by the getData function in the SuspensePage component. The important bit is the last element there, which is the props that will be passed to the component when it's rendered, via a call to createElement.
It looks like the $d is an internal mapping that will resolve to a React Suspense component. Note that if you watched this HTML response streaming in, you'd see all of the <script> tags being rendered pretty much immediately, with the exception of this final one that defines the Suspense component, which comes in 1000ms later per our setTimeout. What this means is that we don't actually see the "Loading..." text in the browser, because the Suspense component is only rendered once the data is available. Take a look at my post on async RSC and Suspense for more on that.
Using a Promise
One other thing we can do is return a Promise from our component. This will allow us to see our loading UI. Let's swap out the page.tsx file for one that does this:
promise/page.tsx
import { Suspense } from "react";
import ClientPromise from "./component";
async function getData(): Promise<any> {
return new Promise((resolve) => {
setTimeout(() => {
resolve("promise resolved data");
}, 1000);
});
}
export default function SuspensePage() {
return (
<div>
<h1>Server Component</h1>
<Suspense fallback={<div>Loading...</div>}>
<ClientPromise dataPromise={getData()} />
</Suspense>
</div>
);
}
promise/page.tsx
import { Suspense } from "react";
import ClientPromise from "./component";
async function getData(): Promise<any> {
return new Promise((resolve) => {
setTimeout(() => {
resolve("promise resolved data");
}, 1000);
});
}
export default function SuspensePage() {
return (
<div>
<h1>Server Component</h1>
<Suspense fallback={<div>Loading...</div>}>
<ClientPromise dataPromise={getData()} />
</Suspense>
</div>
);
}
Note that we removed the async from the SuspensePage function, and we're passing the unresolved Promise directly into our child ClientPromise component, which looks like this (note that this is a client-side component via use client):
Neither our server component nor our client component functions are defined as async, but via the magic of the new use hook we still get asynchronous behavior.
That's kinda crazy. We just sent an uncompleted Promise over the wire from the server to the client, and it... worked? What's going on here? Let's look at the payload (snipped for clarity):
There's our number 8 again. Watching the response streaming in, we can see that this last <script> tag arrived a second after all the others, and it contains the resolved data from our Promise. Suspense is then used to update the page via this little gem:
At the end of the block before, where we have the <div hidden id="S:0">, there was a JavaScript function called $RC that basically replaces the B:0 element on the page with the contents of the S:0 element. This is how the resolved data from the Promise is injected into the page. It's not magic at all, just low-level DOM manipulation.
So in summary, React rendered the fallback, gave it an ID B:0, did some bookkeeping to tell itself that a Promise with ID=8 would be resolving at some point, then when that promise did resolve, it rendered it into a hidden div with ID S:0, as well as into a <script> tag keyed on the Promise ID so that hydration can work, and then executed some inline JavaScript to replace B:0 with S:0.
Thoughts and Further Reading
It's totally not necessary to deeply understand how the cogs go round here, but it is fascinating. RSC Payloads are a little arcane and hard to understand, but ultimately they're not magic. I'm not in love with how complex and proprietary this stuff is, but that won't stop me using it. As it matures I'm sure there will be more articles and documentation explaining how and why it works, but for now there's not a lot to go on other than random blog posts like this one.
Some posts and resources that I found really useful in grokking this stuff are:
There's actually quite a bit more to RSC Payloads than I covered here, but this article is long enough already. I'll write a shorter one soon about Promises and the constraints on what you can and cannot send this way via RSC Payloads.
Share Post:
What to Read Next
To expand your understanding of React Server Components and their practical applications, consider reading Loading Fast and Slow: async React Server Components and Suspense, which explores how Suspense improves performance during asynchronous operations. Additionally, Promises across the void: Streaming data with RSC delves into the innovative use of promises to enhance data streaming from server to client in React, providing a fascinating look at advanced component rendering techniques.