Closed21

RSC勉強会

てずかてずか

第1章 RSCとは

https://ja.react.dev/reference/rsc/server-components

https://zenn.dev/uhyo/articles/react-server-components-multi-stage

https://postd.cc/server-components/

元々の自分の疑問

  • 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>
  )
}
てずかてずか

クライアントでuseAPIを使うことで、サーバーコンポーネントの一部のプロミスをサーバー側で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>);
}
てずかてずか

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コンポーネントにおいて(不要な場合でも)常にハイドレーションが行われていた

レンダリング順序

  1. クライアントからリクエストを受ける
  2. Reactを走らせて全部HTMLにレンダリング、ハイドレーション用のJSも生成
  3. クライアントに送信
  4. クライアントで全部ハイドレーション

RSC以降のSSR(App Router)

レンダリング順序

  1. クライアントからリクエストを受ける
  2. サーバーコンポーネントが静的なコンポーネントとしてレンダリング <= この過程はリクエストのタイミングじゃなくてビルド時(ステップ1の前)とかでもOK
  3. クライアントコンポーネントをレンダリング <= こいつがSSR!?
  4. クライアントに送信
  5. クライアントでクライアントコンポーネントのみハイドレーション
てずかてずか
てずかてずか

RSC Payloadとは

サーバーコンポーネントをレンダリングした結果の生成物

つまり

  1. サーバーコンポーネントのレンダリング
  2. クライアントコンポーネントのレンダリング

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'の子コンポーネントは全てクライアントコンポーネントになってしまう問題

このスクラップは5ヶ月前にクローズされました