RSC勉強会
第1章 RSCとは
元々の自分の疑問
- SSRと何が違うのか?
-
'use client'
はいつ付ければいいの?
公式ドキュメントを読む
①サーバーコンポーネントではない場合
// bundle.js
import marked from 'marked'; // 35.9K (11.2K gzipped)
import sanitizeHtml from 'sanitize-html'; // 206K (63.3K gzipped)
function Page({page}) {
const [content, setContent] = useState('');
// NOTE: loads *after* first page render.
useEffect(() => {
fetch(`/api/content/${page}`).then((data) => {
setContent(data.content);
});
}, [page]);
return <div>{sanitizeHtml(marked(content))}</div>;
}
②サーバーコンポーネントの場合
import marked from 'marked'; // Not included in bundle
import sanitizeHtml from 'sanitize-html'; // Not included in bundle
async function Page({page}) {
// NOTE: loads *during* render, when the app is built.
const content = await file.readFile(`${page}.md`);
return <div>{sanitizeHtml(marked(content))}</div>;
}
比較
①の場合、静的なページをレンダリングするためだけに、75K gzippedのライブラリをバンドルに含める必要がある。さらに、ページのロード後にデータフェッチのリクエストを待つ必要がある。
②の場合、データフェッチはビルド時に一度だけ走る。そのため、クライアントに送信されるのはレンダーの結果のみ。
つまり、コンテンツは最初のページロード時にすぐ表示され、静的コンテンツをレンダーするためだけの高コストなライブラリをバンドルに含めなくともよくなるのです。
サーバーコンポーネントは静的なコンポーネントに対して事前にレンダリングしておく仕組みなので、useState
などのインタラクティブなAPIは使用できない。インタラクティブなAPIを使用するには'use client'
を使用する必要がある。
"use client"
export default function Expandable({children}) {
const [expanded, setExpanded] = useState(false);
return (
<div>
<button
onClick={() => setExpanded(!expanded)}
>
Toggle
</button>
{expanded && children}
</div>
)
}
クライアントでuse
APIを使うことで、サーバーコンポーネントの一部のプロミスをサーバー側でawait
して、一部のプロミスはクライアント側でサスペンドさせるという技を使えるらしい。(若干応用感があるので詳細はスキップ)
// Server Component
import db from './database';
async function Page({id}) {
// Will suspend the Server Component.
const note = await db.notes.get(id);
// NOTE: not awaited, will start here and await on the client.
const commentsPromise = db.comments.get(note.id);
return (
<div>
{note}
<Suspense fallback={<p>Loading Comments...</p>}>
<Comments commentsPromise={commentsPromise} />
</Suspense>
</div>
);
}
// Client Component
"use client";
import {use} from 'react';
function Comments({commentsPromise}) {
// NOTE: this will resume the promise from the server.
// It will suspend until the data is available.
const comments = use(commentsPromise);
return comments.map(commment => <p>{comment}</p>);
}
uhyoさんの記事を読む
一言で言うと、React Server Componentsは多段階計算です。
SSRという技術もありましたが、これはクライアント向けのコードを無理やりサーバーサイドでも実行するものです。
RSCメンタルモデル
Q. なぜ人々はフロントエンドでJavaScriptを使うのでしょうか?
A. UXのため。ユーザー操作に対して最速でフィードバックを返すため
その目的のためにJavaScriptを使ってReactを書くのはオーバーヘッドが大きすぎるのではないか?と人類が気付き始めた。(Reactをただのテンプレートエンジンのように使っている箇所も多い)
それならば
UXのためにはクライアントサイドのJavaScriptが必要だが、UXに関係ない部分はサーバーサイドで処理したほうが良い
1段階目の計算
テンプレートエンジンとして使われているReactコンポーネント(静的なコンポーネント)を先にレンダリング
2段階目の計算
ユーザーの操作にフィードバックを返す部分は従来通りレンダリング
他の良さそうな記事を読む
SSRとRSC
Next.jsでの例
従来のSSR(Page Router)
SSRという技術もありましたが、これはクライアント向けのコードを無理やりサーバーサイドでも実行するものです。 by uhyoさん
本来クライアントで行われる想定のReactレンダリングをNext.jsがサーバーサイドで行うことで(無理やり?)SSRを実現していた
従来のSSRがすごかったところ
-
getServerSideProps
はクライアント上で再実行されず、バンドルファイルにも含まれない
→ 当時かなり革命的だったのでは?
従来のSSRの問題点
-
getServerSideProps
関数はルートレベルでしか機能しない(どのコンポーネント内部でも実行できるわけではない) - フレームワークによってSSRのアプローチがバラバラ
- 全てのReactコンポーネントにおいて(不要な場合でも)常にハイドレーションが行われていた
レンダリング順序
- クライアントからリクエストを受ける
- Reactを走らせて全部HTMLにレンダリング、ハイドレーション用のJSも生成
- クライアントに送信
- クライアントで全部ハイドレーション
RSC以降のSSR(App Router)
レンダリング順序
- クライアントからリクエストを受ける
- サーバーコンポーネントが静的なコンポーネントとしてレンダリング <= この過程はリクエストのタイミングじゃなくてビルド時(ステップ1の前)とかでもOK
- クライアントコンポーネントをレンダリング <= こいつがSSR!?
- クライアントに送信
- クライアントでクライアントコンポーネントのみハイドレーション
第二章 RSCの仕組み
RSC Payloadとは
サーバーコンポーネントをレンダリングした結果の生成物
つまり
- サーバーコンポーネントのレンダリング
- クライアントコンポーネントのレンダリング
uhyoさんの記事にあったようにこの2段階でコンポーネントが計算されるとすると、1段階目の生成物。このRSC Payloadを用いて2段階目の計算が行われる
RSC Payloadが含むもの
- サーバーコンポーネントのレンダリング結果(厳密にはHTMLではない)
- クライアントコンポーネントのレンダリング場所と、実行すべきJSファイルへの参照
- サーバーコンポーネントからクライアントコンポーネントに渡されたprops
RSCの例
[
{
"identifier":0,
"type":null,
"data":[
"OaPKRBs0KvtlR-v4ORTOM",
[
[
"children",
"(main)",
"children",
"blog",
"... (more content)"
]
]
]
},
{
"identifier":3,
"type":"I",
"data":{
"id":47767,
"chunks":[
"2272:static/chunks/webpack-7dc9770b4e094816.js",
"2971:static/chunks/fd9d1056-ea8ad81a8bf99663.js",
"596:static/chunks/596-5ca25ac509d95d33.js"
],
"name":"",
"async":false
}
},
{
"identifier":4,
"type":"I",
"data":{
"id":57920,
"chunks":[
"2272:static/chunks/webpack-7dc9770b4e094816.js",
"2971:static/chunks/fd9d1056-ea8ad81a8bf99663.js",
"596:static/chunks/596-5ca25ac509d95d33.js"
],
"name":"",
"async":false
}
},
{
"identifier":5,
"type":"I",
"data":{
"id":46685,
"chunks":[
"6685:static/chunks/6685-a4c378aab6a445df.js",
"3517:static/chunks/app/(main)/blog/page-5ef(...).js"
],
"name":"",
"async":false
}
},
{
"identifier":6,
"type":null,
"data":"$Sreact.suspense"
},
{
"identifier":1,
"type":"",
"data":[
"$",
"$L3",
null,
{
"parallelRouterKey":"children",
"segmentPath":[
"children",
"... (more content)"
]
}
]
},
{
"identifier":2,
"type":null,
"data":[
[
"$",
"meta",
"0",
{
"charSet":"utf-8"
}
],
[
"$",
"title",
"1",
{
"children":"Blog"
}
],
"... (more content)"
]
},
{
"identifier":7,
"type":null,
"data":[
"$",
"div",
null,
{
"className":"inflate-y-8 ...",
"children":[
[
"$",
"section",
"2022",
{
"className":"flex flex-col items-start",
"children":[
[
"$",
"h3",
null,
{
"className":"font-heading text-3xl ...",
"children":"2022"
}
],
"... (more content)"
]
}
]
]
}
]
}
]
また、転送量だけ見るとServer Componentのほうが大きいとはいえ、Server Component版のほうがクライアントで実行されるコンポーネントが少ないため、hydration速度で勝るケースもあるかもしれません(調べたところ今回のアプリケーションではそうでもありませんでしたが)。
筆者としては、Server Componentに寄せることの設計上のメリットが大きいと感じているため、これくらいの転送量増加であれば気にせずServer Componentを使用します。
静的なコンポーネントは、できるだけServer Component
で実装しよう
この考えで良さそうかな?
クライアントコンポーネントはサーバコンポーネントをインポートできない
'use client'
の子コンポーネントは全てクライアントコンポーネントになってしまう問題
Compositionパターンを使うことで解決できる可能性
RSC完全に理解した