1
/
5

【App Router】React Server Component Payloadをちょっとだけ理解しよう【株式会社ライトコード】


はじめに

前回の記事から引き続き、Next.jsのApp Router周りを見ていきたいと思います。
前回の記事ではApp Routerで使用できるCache周りを確認していきましたが、その中で React Server Component Payload (以下 RSC Payload)という言葉が出てきました。
今回の記事ではこのRSC Payloadとは何か、どんな情報が入っているのかについて確認していきたいと思います。

RSC Payloadとは何か

RSC PayloadとはServer Component Treeをレンダリングする際に生成される特別なデータフォーマットです。
公式ドキュメントを見ると、Next.jsではReactのAPIを使用して個別のルートごとのPayloadとその中のSuspense BoundaryごとのPayloadをチャンクに分けて生成されるようです。
生成されたRSC Payloadは

  • SSR時のServer側でのHTML生成
  • Client側でのServer Component・Client Componentでの擦り合わせをしてDOM更新(Hydration)

で使用されます。
ざっくりとした概要がわかったところで、次は実際にどんな値がRSC Payloadとして生成されるか見てみましょう。

RSC Payloadの中身

RSC Payloadには以下の内容が入っています。

  1. レンダリングされたHTML(の要素)
  2. Server ComponentからClient Componentに渡されるprops
  3. レンダリングするClient Componentのプレスホルダー
  4. Client ComponentへのJSファイルの参照

(リスト1)

では実際に生成されるRSC Payloadの中身を確認するために、試しにNext.js(v14.2.4)でServer Componentを使用した簡単なページを作成してみましょう。
内容は以下になります。

// app/layout.tsx --------------------------------

import type { Metadata } from "next";

export const metadata: Metadata = {
title: "Simple Page",
};

export default function RootLayout({
children,
}: Readonly) {
return (
<html>
<body>{children}</body>
</html>
);
}

// app/page.tsx --------------------------------
import ServerComponent from "./_components/ServerComponent";

export default function SSGPage() {
return (
<main>
<h1>Static Generated Page</h1>
<ServerComponent />
</main>
);
}


// app/_components/ServerComponent.tsx --------------------------------
import ClientButtonComponent from "./ClientButtonComponent";

export default function ServerComponent() {
return (
<div>
<p>Server Component</p>
<ClientButtonComponent label="Client Button" />
</div>
);
}


// app/_components/ClientButtonComponent.tsx --------------------------------
"use client";

export default function ClientButtonComponent({ label }: { label: string }) {
return (
<button
onClick={() => {
alert("clicked!!");
}}
>
{label}
</button>
);
}

上記Pageをビルドして生成されたRSC Payloadは下記のようになっています。

2:I[1642,["931","static/chunks/app/page-11ba5384ffce2cd6.js"],"default"]

3:I[9275,[],""]
4:I[1343,[],""]
0:["bjkOWKKLqKIDRfpXBwojp",[[["",{"children":["__PAGE__",{}]},"$undefined","$undefined",true],["",{"children":["__PAGE__",{},[["$L1",["$","main",null,{"children":[["$","h1",null,{"children":"Static Generated Page"}],["$","div",null,{"children":[["$","p",null,{"children":"Server Component"}],["$","$L2",null,{"label":"Client Button"}]]}]]}]],null],null]},[["$","html",null,{"children":["$","body",null,{"children":["$","$L3",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L4",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","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."}]}]]}]}]],"notFoundStyles":[],"styles":null}]}]}],null],null],[null,"$L5"]]]]
5:[["$","meta","0",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","meta","1",{"charSet":"utf-8"}],["$","title","2",{"children":"Simple Page"}],["$","link","3",{"rel":"icon","href":"/favicon.ico","type":"image/x-icon","sizes":"16x16"}]]
1:null

このままの生のRSC Payloadでは情報が見づらいので、rsc-parserというツールを使用してよりわかりやすい形にしてみましょう。
下記はrsc-parserを使用して 0:[…] の行をパースしたものになります。

bjkOWKKLqKIDRfpXBwojp

{
children:
__PAGE__
{
}
}
undefined
undefined
true
{
children:
__PAGE__
{
}
1 (L - Lazy node)
<main>
<h1>
Static Generated Page
</h1>
<div>
<p>
Server Component
</p>
<2 (L - Lazy node)
label="Client Button"
/>
</div>
</main>
{null}
{null}
}
<html>
<body>
<3 (L - Lazy node)
parallelRouterKey="children"
segmentPath={
[ "children" ]
}
template={
<4 (L - Lazy node) />
}
notFound={
[
<title>
404: This page could not be found.
</title>
,
<div
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"
}}
>
<div>
<style
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
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"
}}
>
404
</h1>
<div
style={{
display: "inline-block"
}}
>
<h2
style={{
fontSize: 14,
fontWeight: 400,
lineHeight: "49px",
margin: 0
}}
>
This page could not be found.
</h2>
</div>
</div>
</div>
]
}
notFoundStyles={
[]
}
styles={null}
/>
</body>
</html>
null
null
null
5 (L - Lazy node)

0:[…] の行を確認してみると、pageの中身をレンダリングした値が含まれていました!

<main>

<h1>
Static Generated Page
</h1>
<div>
<p>
Server Component
</p>
<2 (L - Lazy node)
label="Client Button"
/>
</div>
</main>

ServerComponentの中身はサーバー側でレンダリングされて、ただのHTMLとして出力されていることがわかります。(リスト1の1)
対してClientComponentが埋め込まれていた箇所には、

<2 (L - Lazy node) 

label="Client Button"
/>

という不思議な値になっています。(上記はrsc-parseの独自の記法)
label="Client Button" の部分はServerComponentからClientComponentへ渡したpropsのようです。(リスト1の2)
では、2 (L - Lazy node) の部分はどういう意味を持っているのでしょうか?
RSC Payloadの記法を確認しながら読み取っていくことにしましょう。

💡 ちなみに0の行のpageのレンダリング結果以外のところや他の行には、MetaデータやNext.jsの必須のClient Componentの情報などが入ってるようでした。

記事の続きは下のURLをクリック!

https://rightcode.co.jp/blogs/47568

エンジニア積極採用中です!

現在、WEBエンジニア、モバイルエンジニア、デザイナー、営業などを積極採用中です!

採用ページはこちら:https://rightcode.co.jp/recruit

社員の声や社風などを知りたい方はこちら:https://rightcode.co.jp/blogs?category=life

社長と一杯飲みながらお話しませんか?(転職者向け)

特設ページはこちら: https://rightcode.co.jp/gohan-sake-president-talk

もっとワクワクしたいあなたへ

現在、ライトコードでは「WEBエンジニア」「モバイルエンジニア」「ゲームエンジニア」、「デザイナー」「WEBディレクター」「営業」などを積極採用中です!

ライトコードは技術力に定評のある受託開発をメインにしているIT企業です。

有名WEBサービスやアプリの受託開発などの企画、開発案件が目白押しの状況です。

  • もっと大きなことに挑戦したい!
  • エンジニアとしてもっと成長したい!
  • モダンな技術に触れたい!

現状に満足していない方は、まずは、エンジニアとしても第一線を走り続ける弊社代表と気軽にお話してみませんか?

ネット上では、ちょっとユルそうな会社に感じると思いますが(笑)、
実は技術力に定評があり、沢山の実績を残している会社ということをお伝えしたいと思っております。

  • ライトコードの魅力を知っていただきたい!
  • 社風や文化なども知っていただきたい!
  • 技術に対して熱意のある方に入社していただきたい!

一度、【Wantedly内の弊社ページ】や【コーポレートサイト】をのぞいてみてください。

【コーポレートサイト】https://rightcode.co.jp/

【採用募集】https://rightcode.co.jp/recruit(こちらからの応募がスムーズ)

【wantedlyぺージ】https://www.wantedly.com/companies/rightcode


株式会社ライトコード's job postings

Weekly ranking

Show other rankings
Invitation from 株式会社ライトコード
If this story triggered your interest, have a chat with the team?