【App Router】React Server Component Payloadをちょっとだけ理解しよう
IT技術
はじめに
前回の記事から引き続き、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には以下の内容が入っています。
- レンダリングされたHTML(の要素)
- Server ComponentからClient Componentに渡されるprops
- レンダリングするClient Componentのプレスホルダー
- Client ComponentへのJSファイルの参照
(リスト1)
では実際に生成されるRSC Payloadの中身を確認するために、試しにNext.js(v14.2.4)でServer Componentを使用した簡単なページを作成してみましょう。
内容は以下になります。
1// app/layout.tsx --------------------------------
2import type { Metadata } from "next";
3
4export const metadata: Metadata = {
5 title: "Simple Page",
6};
7
8export default function RootLayout({
9 children,
10}: Readonly) {
11 return (
12 <html>
13 <body>{children}</body>
14 </html>
15 );
16}
17
18// app/page.tsx --------------------------------
19import ServerComponent from "./_components/ServerComponent";
20
21export default function SSGPage() {
22 return (
23 <main>
24 <h1>Static Generated Page</h1>
25 <ServerComponent />
26 </main>
27 );
28}
29
30
31// app/_components/ServerComponent.tsx --------------------------------
32import ClientButtonComponent from "./ClientButtonComponent";
33
34export default function ServerComponent() {
35 return (
36 <div>
37 <p>Server Component</p>
38 <ClientButtonComponent label="Client Button" />
39 </div>
40 );
41}
42
43
44// app/_components/ClientButtonComponent.tsx --------------------------------
45"use client";
46
47export default function ClientButtonComponent({ label }: { label: string }) {
48 return (
49 <button
50 onClick={() => {
51 alert("clicked!!");
52 }}
53 >
54 {label}
55 </button>
56 );
57}
上記Pageをビルドして生成されたRSC Payloadは下記のようになっています。
12:I[1642,["931","static/chunks/app/page-11ba5384ffce2cd6.js"],"default"]
23:I[9275,[],""]
34:I[1343,[],""]
40:["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"]]]]
55:[["$","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"}]]
61:null
このままの生のRSC Payloadでは情報が見づらいので、rsc-parserというツールを使用してよりわかりやすい形にしてみましょう。
下記はrsc-parserを使用して 0:[…]
の行をパースしたものになります。
1bjkOWKKLqKIDRfpXBwojp
2{
3children:
4__PAGE__
5{
6}
7}
8undefined
9undefined
10true
11{
12children:
13__PAGE__
14{
15}
161 (L - Lazy node)
17<main>
18 <h1>
19 Static Generated Page
20 </h1>
21 <div>
22 <p>
23 Server Component
24 </p>
25 <2 (L - Lazy node)
26 label="Client Button"
27 />
28 </div>
29</main>
30{null}
31{null}
32}
33<html>
34 <body>
35 <3 (L - Lazy node)
36 parallelRouterKey="children"
37 segmentPath={
38 [ "children" ]
39 }
40 template={
41 <4 (L - Lazy node) />
42 }
43 notFound={
44 [
45 <title>
46 404: This page could not be found.
47 </title>
48 ,
49 <div
50 style={{
51 fontFamily: "system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"",
52 height: "100vh",
53 textAlign: "center",
54 display: "flex",
55 flexDirection: "column",
56 alignItems: "center",
57 justifyContent: "center"
58 }}
59 >
60 <div>
61 <style
62 dangerouslySetInnerHTML={{
63 "__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)}}"
64 }}
65 />
66 <h1
67 className="next-error-h1"
68 style={{
69 display: "inline-block",
70 margin: "0 20px 0 0",
71 padding: "0 23px 0 0",
72 fontSize: 24, fontWeight: 500,
73 verticalAlign: "top",
74 lineHeight: "49px"
75 }}
76 >
77 404
78 </h1>
79 <div
80 style={{
81 display: "inline-block"
82 }}
83 >
84 <h2
85 style={{
86 fontSize: 14,
87 fontWeight: 400,
88 lineHeight: "49px",
89 margin: 0
90 }}
91 >
92 This page could not be found.
93 </h2>
94 </div>
95 </div>
96 </div>
97 ]
98 }
99 notFoundStyles={
100 []
101 }
102 styles={null}
103 />
104 </body>
105</html>
106null
107null
108null
1095 (L - Lazy node)
0:[…]
の行を確認してみると、pageの中身をレンダリングした値が含まれていました!
1<main>
2 <h1>
3 Static Generated Page
4 </h1>
5 <div>
6 <p>
7 Server Component
8 </p>
9 <2 (L - Lazy node)
10 label="Client Button"
11 />
12 </div>
13</main>
ServerComponentの中身はサーバー側でレンダリングされて、ただのHTMLとして出力されていることがわかります。(リスト1の1)
対してClientComponentが埋め込まれていた箇所には、
1<2 (L - Lazy node)
2 label="Client Button"
3/>
という不思議な値になっています。(上記はrsc-parseの独自の記法)
label="Client Button"
の部分はServerComponentからClientComponentへ渡したpropsのようです。(リスト1の2)
では、2 (L - Lazy node)
の部分はどういう意味を持っているのでしょうか?
RSC Payloadの記法を確認しながら読み取っていくことにしましょう。
💡 ちなみに0の行のpageのレンダリング結果以外のところや他の行には、MetaデータやNext.jsの必須のClient Componentの情報などが入ってるようでした。 |
RSC Payloadの記法(一部)
RSC Payloadの各行は ID:TYPE?JSON
という形式になっているようです。(TYPEはない場合もあるので ?
マークで表しています)
今回出力されたRSC Payloadを見るとTYPEに当たる部分は I
とタイプなしの二つがありました。
では2つのタイプはどのように意味を持っているのでしょうか?
I
タイプ:
モジュールを表しており、I
に続くJSONの値はJSファイルの参照になっておりこのファイルを読み込んでレンダリングすることを表しています。- タイプなし:
タイプが存在しない行のJSONにはhtmlのタグ情報やpropsなどの情報が含まれており、タイプなしはReactのエレメント情報であることを表しています。
では、本題のClient Componentが置き変わっていた箇所の 2 (L - Lazy node)
はどのような意味なのでしょうか?
parse前の対象の値は ["$","$L2",null,{"label":"Client Button"}]
となっており、L
はLazy タイプを表し(先ほど見たタイプとは別物)、2
は参照する他のPayloadの ID
を指しています。(リスト1の3)
Lazyタイプは、SuspenseやClient ComponentなどPromiseを伴うものに使用されます。(Client Componentの場合は対象のファイルが読み込まれるまで待機するためPromiseが伴う)
今回の "$L2"
では、ID
が 2
のPayloadを参照し、そのPayloadに記載されているJSファイルの読み込みが完了してから処理するという意味になります。
ID
が 2
のPayloadは 2:I[1642,["931","static/chunks/app/page-11ba5384ffce2cd6.js"],"default"]
にあたるので、static/chunks/app/page-11ba5384ffce2cd6.js
のファイルが読み込みを待つということになります。(リスト1の4)
ファイルの読み込みが完了し処理された値は、$L2
と記載されていたServer Comopnentの中に埋め込まれるため、ブラウザ側でもServer Component・Client Componentの連携を正しく処理できるということになります。
今回生成されたRSC Payloadの中に含まれていたタイプ以外にも色々なタイプは存在していますが、全てを見ていくと記事が長くなってしまうので一部だけ紹介させたいただきました。
まとめ
さて、この記事ではRSC Payloadの中にどんな値が含まれているのかなどをざっと確認してきました。
Next.jsを使用する上でここまで知っておかなくても問題なく業務をすることはできますが、どのようにServer ComponentとClient Componentが連携しているかのイメージがよりついた気がします。
実際にNext.jsがこのRSC Payloadをどのように使用して、サーバー・クライアント側でStreamingなどを含めたレンダリングを行っているのかまで調べたかったのですが、それはまた別の機会に記事にできればと思います。(ソースコードなど読んでみたのですが、難しすぎて今回は諦めました😇)
クライアント側でのサーバーからデータ取得してレンダリングまでの間にキャッシュにデータをセットしたりなど色々行っているようでしたので(Router Cacheの処理っぽい?)、ここを理解できればNext.jsのキャッシュなどを含めたレンダリングの全貌がイメージできるのかもしれません。
ではではまた次の記事でお会いしましょう!
参考記事
https://www.alvar.dev/blog/creating-devtools-for-react-server-components
https://jser.dev/react/2023/04/20/how-do-react-server-components-work-internally-in-react
ライトコードでは、エンジニアを積極採用中!
ライトコードでは、エンジニアを積極採用しています!社長と一杯しながらお話しする機会もご用意しております。そのほかカジュアル面談等もございますので、くわしくは採用情報をご確認ください。
採用情報へ
Udemy信奉者系フロントエンジニア(バックエンドもちょっと)。 現在はNextjsを用いた不動産情報サイトのフロントエンド開発担当中。 映画好きで基本毎日Netflixしてます。