tips chips

日々の作業で出てきた技術メモの切れ端を置いておくページ

React Server Componentsについて調べたメモ

RFCを読みつつ自分の理解を書いたり、実験したコードが載ってたりする。

React Server Componentsについてざっくりと理解するためのメモ。

何が嬉しい技術なのか

bundleのサイズ削減

参考: https://github.com/reactjs/rfcs/blob/main/text/0188-server-components.md#zero-bundle-size-components

  • アプリケーションの静的にレンダリングすれば良い部分をサーバー側にやらせることができる(ユーザーのインタラクションによって変化しない部分)
  • MarkdownのRenderやDateのフォーマットなどのライブラリは容量が大きくなりがちである→これらのレンダリングをサーバー側にやらせて結果だけをクライアントに返すことで、クライアントサイドではライブラリをダウンロードする必要すらなくなる。
  • 「インタラクティブなコンポーネントは従来通りクライアントにやらせて、そうではない静的な部分はサーバーサイドが受け持つ」ということができるのがRSCの新しい部分である(という風に理解した)

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ではHTTP APIだけでなく、ローカルのファイル・非公開のAPI・DBなどあらゆるデータソースを使える
  • originに近いところでfetchすることによるパフォーマンス向上も見込める

また、RSCではPromiseを返す関数をコンポーネントとして使えるようになった(参考: RFC)

シンプルに書きやすくて良い。アドベントカレンダーの記事にも書いたが、再帰的に呼び出すコンポーネントとかでめちゃくちゃよかった。

SSRとは違うのか?

目的が違う(と思う)

それぞれの技術の目的を整理すると…

  • SSR
    • 初期描画を速くする → hydrateの時に、レンダリング済みコンポーネントの実装がダウンロードされる(ので最終的なバンドルサイズの削減が目的ではない)
    • JavaScriptが使えない環境(SNSのOGP表示のためのリクエストなど)に向けてレンダリング結果を返す
  • RSC
    • クライアントサイドにダウンロードされるJSの削減
    • サーバーサイドでのレンダリングによるパフォーマンスの向上

RSC自体はSSRとの組み合わせはマストではない。

Next.jsではRSCとSSRを組み合わせている。

どのように実現されているのか

  1. クライアントはサーバーに対してリクエストをする
  2. サーバーは対応するパスのコンポーネントをレンダリングする。レンダリングの結果をクライアントにストリーミングする。
    1. この時Server Componentsはネイティブコンポーネントになるまでレンダリング、クライアントコンポーネントはそのまま
  3. クライアントはサーバーからのレスポンスを解釈しながら、レンダリングを行なっていく
    1. 完全にレスポンスが返ってきていなくても逐次レンダリングをする

サーバーは必ずしもツリーの一番上から順番に送信するわけではなく、順次レンダリングできたところから順番に送信する。

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に対応するものはレンダリングされて内容が返ってくる(ここでいうと J5J6 )
逆に、クライアントコンポーネントはコンポーネントの参照だけが返ってくる。(
M7 )これを見てReactランタイムはコンポーネントを取得しにいく。

面白いのはJ1(ページ全体のDOMを表す行)より先にM7があるということ、このクライアントコンポーネントが必要になるのがわかっているから先に読み込んでおくようにクライアントサイドに指示している感じ。

今回、疑似的にめちゃくちゃfetchに時間がかかるコンポーネントを用意した。その場合のストリームを見ると以下のようになる。

J6 の行だけ3秒待ってから返ってくるが、それ以外の部分は先に返ってきているのがわかる。(ちなみにNext.jsでは rsc というヘッダーをつけてリクエストをするとレンダリングされたHTMLではなくRSCのストリームが返ってくる)

rfcs/text/0188-server-components.md at main · reactjs/rfcs
RFCs for changes to React. Contribute to reactjs/rfcs development by creating an account on GitHub.
https://github.com/reactjs/rfcs/blob/main/text/0188-server-components.md