React Server Componentsについて調べたメモ
RFCを読みつつ自分の理解を書いたり、実験したコードが載ってたりする。
日々の作業で出てきた技術メモの切れ端を置いておくページ
RFCを読みつつ自分の理解を書いたり、実験したコードが載ってたりする。
React Server Componentsについてざっくりと理解するためのメモ。
参考: https://github.com/reactjs/rfcs/blob/main/text/0188-server-components.md#zero-bundle-size-components
React runtime will be loaded, which is cacheable and predictable in size.
(Next.js betaドキュメントより)
アプリケーションの規模が大きくなってもランタイムの大きさは一定、かつキャッシュすることもできる
combining the rich interactivity of client-side apps with the improved performance of traditional server rendering:
(RFCより)
クライアントサイドで動くリッチなインタラクティブ性と昔ながらのサーバーレンダリングによるパフォーマンス向上の両立ができる。
参考: https://github.com/reactjs/rfcs/blob/main/text/0188-server-components.md#full-access-to-the-backend
また、RSCではPromiseを返す関数をコンポーネントとして使えるようになった(参考: RFC)
シンプルに書きやすくて良い。アドベントカレンダーの記事にも書いたが、再帰的に呼び出すコンポーネントとかでめちゃくちゃよかった。
目的が違う(と思う)
それぞれの技術の目的を整理すると…
RSC自体はSSRとの組み合わせはマストではない。
Next.jsではRSCとSSRを組み合わせている。
サーバーは必ずしもツリーの一番上から順番に送信するわけではなく、順次レンダリングできたところから順番に送信する。
import React from 'react'
import Slow from '../components/Slow'
import Fast from '../components/Fast'
import Client from '../components/Client'
import Loading from '../components/Loading'
export default function Home() {
return (
<main>
<React.Suspense fallback={<Loading />}>
{/* Server Components(速い) */}
<Fast />
</React.Suspense>
<React.Suspense fallback={<Loading />}>
{/* Server Components(fetchに3秒かかる) */}
<Slow />
</React.Suspense>
{/* Client Components */}
<Client />
</main>
)
}
上記のようなページをレンダリングすると
J0:[[["",{"children":["",{}]},null,null,true],"@1",[["$","title",null,{"children":"Create Next App"}],["$","meta",null,{"content":"width=device-width, initial-scale=1","name":"viewport"}],["$","meta",null,{"name":"description","content":"Generated by create next app"}],["$","link",null,{"rel":"icon","href":"/favicon.ico"}]]]]
M2:{"id":"(app-client)/./node_modules/next/dist/client/components/layout-router.js","name":"","chunks":["app-client-internals:app-client-internals"],"async":false}
M3:{"id":"(app-client)/./node_modules/next/dist/client/components/render-from-template-context.js","name":"","chunks":["app-client-internals:app-client-internals"],"async":false}
S4:"react.suspense"
M7:{"id":"(app-client)/./components/Client.tsx","name":"","chunks":["app/page:app/page"],"async":false}
J1:[[],[],["$","html",null,{"lang":"en","children":[["$","head",null,{}],["$","body",null,{"children":["$","@2",null,{"parallelRouterKey":"children","segmentPath":["children"],"hasLoading":false,"template":["$","@3",null,{}],"notFound":["$","div",null,{"style":{"fontFamily":"-apple-system, BlinkMacSystemFont, Roboto, \"Segoe UI\", \"Fira Sans\", Avenir, \"Helvetica Neue\", \"Lucida Grande\", sans-serif","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":[["$","head",null,{"children":["$","title",null,{"children":"404: This page could not be found."}]}],["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"\n body { margin: 0; color: #000; background: #fff; }\n .next-error-h1 {\n border-right: 1px solid rgba(0, 0, 0, .3);\n }\n\n @media (prefers-color-scheme: dark) {\n body { color: #fff; background: #000; }\n .next-error-h1 {\n border-right: 1px solid rgba(255, 255, 255, .3);\n }\n }\n "}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":0,"marginRight":"20px","padding":"0 23px 0 0","fontSize":"24px","fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block","textAlign":"left","lineHeight":"49px","height":"49px","verticalAlign":"middle"},"children":["$","h2",null,{"style":{"fontSize":"14px","fontWeight":"normal","lineHeight":"49px","margin":0,"padding":0},"children":"This page could not be found."}]}]]}]]}],"childProp":{"current":[[],[],["$","main",null,{"children":[["$","$4",null,{"fallback":["$","div",null,{"children":"Loading...🫠"}],"children":"@5"}],["$","$4",null,{"fallback":["$","div",null,{"children":"Loading...🫠"}],"children":"@6"}],["$","@7",null,{}]]}]],"segment":""},"rootLayoutIncluded":true}]}]]}]]
J5:["$","div",null,{"children":"This is fast component🚀"}]
J6:["$","div",null,{"children":"This is Slow component🐢"}]
サーバーはクライアントに対してこんな感じの1行ごとに記号とJSONが含まれたレスポンスを返し、クライアントではこれを解釈している。
J1から始まる行が今回の場合ページコンポーネントで、CSSとかが入っているので横に長いが最後の方を見ると
[["$","$4",null,{"fallback":["$","div",null,{"children":"Loading...🫠"}],"children":"@5"}],["$","$4",null,{"fallback":["$","div",null,{"children":"Loading...🫠"}],"children":"@6"}],["$","@7",null,{}]]
というような部分がある。
@から始まっているのがコンポーネントへの参照で、 @5
@6
@7
というコンポーネントをrenderingしなさいというような意味になっている(fallback
とか書いてるのはSuspenseを使っているため。fallbackに渡したコンポーネントもRSCなのでレンダリングされてここに返ってきている)
@5
@6
@7
というのはそれぞれ、ストリームの M5
M6
M7
の行と対応しているため、該当する行がストリームされてきたら順次置き換えられていく。
Mから始まる行のうち、RSCに対応するものはレンダリングされて内容が返ってくる(ここでいうと J5
と J6
)
逆に、クライアントコンポーネントはコンポーネントの参照だけが返ってくる。( M7
)これを見てReactランタイムはコンポーネントを取得しにいく。
面白いのはJ1(ページ全体のDOMを表す行)より先にM7があるということ、このクライアントコンポーネントが必要になるのがわかっているから先に読み込んでおくようにクライアントサイドに指示している感じ。
今回、疑似的にめちゃくちゃfetchに時間がかかるコンポーネントを用意した。その場合のストリームを見ると以下のようになる。
J6
の行だけ3秒待ってから返ってくるが、それ以外の部分は先に返ってきているのがわかる。(ちなみにNext.jsでは rsc
というヘッダーをつけてリクエストをするとレンダリングされたHTMLではなくRSCのストリームが返ってくる)