【衝撃】React Server Componentsが転送量に与えた驚きの影響とは……

皆さんこんにちは。最近React界隈を騒がせている**React Server Components** (**RSC**) には、筆者も興味を寄せています。以下の記事でもRSCについて説明しました。

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

この記事の中で少し言及しているのが転送量の話です。

というのも、RSCの利点として挙げられているもののうち、パフォーマンスに関係するものとして**バンドルサイズの削減**があります。つまり、Server Componentはクライアントに送られる前にただのHTMLになってしまうため、コンポーネントの定義を送る必要がない分だけJavaScriptコードの量が減るということです。

しかし、実は話はそう単純ではありません。よくよく考えると、Server ComponentがただのHTMLになると、**逆にサイズが増える**ということも考えられます。

```jsx
// Reactコードがこれなのに
<Button>click me!</Button>

// ただのHTMLになったらこうなっているかも……
<button type="submit" className="inline-flex flex flex-col px-3 py-2 text-center justify-center items-center"><span>click me!</span></button>
```

したがって、Server Componentを使うと無条件に転送量が減るということにはなりません。前述の記事で述べている通り、実装の改善が進めば根本的解決が見られるだろうと筆者は思っていますが、今のところは転送量が実際どうなるのか気に掛ける必要があります。

## 実験

ということで簡単な実験を行ってみたので、この記事ではその結果をご紹介します。

同じコンテンツを常時するページを2つ用意し、コンポーネントツリーの一部を片方ではServer Componentに、もう片方ではClient Componentとします。両者にアクセスしたときの転送量を比較するという実験です。

先に述べておくと、結果は正直面白くないもので、**ぜんぜん衝撃的ではありません**。しかし、実験してしまった以上報告する必要があるので記事を用意しました。

## 実験の内容

実験はこちらにデプロイされたNext.jsアプリで行なっています。

https://rsc-transfer-size-exp.vercel.app/

表示するとポケモンの一覧（1000匹くらい）が表示されます。データの提供元は例によって[PokéAPI](https://pokeapi.co/)です。

上のURLでは全てServer Componentでレンダリングされているため、約1000匹分のHTMLが転送されてきています。1匹分のコードは次のようになっています。Server Componentが転送量に与える影響が大きくなるように、Tailwind CSSを用いてスタイリングしています。

```html
<div class="grid grid-rows-2 w-40 p-2 bg-yellow-100">
  <span class="row-start-1 col-start-1 justify-self-end text-gray-600">#1</span>
  <span class="row-start-1 col-start-1 font-bold">フシギダネ</span>
  <span class="row-start-2 text-sm">Bulbasaur</span>
</div>
```

一方、もう一つ用意した次のURLでは、1000匹分のデータはClient Componentでレンダリングされます。

https://rsc-transfer-size-exp.vercel.app/client

つまり、こちらのURLでは、上のHTMLの代わりに次のような状態で転送されています（実際には、上のHTMLも下のコードもいわゆるReact Flightのプロトコルに従う形で`<script>`内に埋め込まれています）。

```jsx
<PokemonBox key={pokemon.id} pokemon={pokemon} />
```

下のリンクでは1000匹分のデータは生のHTMLではなくJSONの形で転送されており、それを1000匹分のHTMLに展開するのはクライアント側（Client Component）で行われます。

この2つのページを開いたときの転送量を比較するのが今回の実験です。

## 仮説

この比較では、Server Componentを使ったほうが展開後のコード量が多くなるのは疑いようがありません。しかし、実際にデータが転送される際は圧縮が行われるので、 **圧縮を加味すれば実はそんなに増えないのでは？** という仮説を持っていました。

## 結果

ということで、実験結果です。下の表の容量は全てBrotliでの圧縮後のデータ量です。両者で共通のリソースは省略しています。

| | Server Component | Client Component |
|-| - | - |
| HTML | 39.0 kB | 35.4 kB |
| page-8f46a23f35fd2dea.js | - | 1.1 kB |
| 合計 | 39.0 kB | 36.5 kB |

結果を見ると、圧縮を加味しても、Server Component版のほうがHTMLが 3.6 kB増加しています。ただし、Client Component版では送られてきていた 1.1 kB のファイルが不要になっています。この部分がServer Componentによるバンドルサイズの削減分です。

ということで、上のアプリの場合はServer Componentの方が 2.5 kB だけ転送量が多い結果になりました。

結局**Server Component版の方が転送量が多い**ということですね。がっかりです。

とはいえ、圧縮前のHTMLのサイズで比較すると、Server Component版が 673 kB、Client Component版が 330 kB と数百kB単位の差がありました。それを数kBまで縮めてくれる圧縮の効果はやはり大きく、Server Componentの実用性を支えていますね。

## 考察

結果のところで見たように、今回のアプリケーションではServer Componentを使ったほうが転送量がやや大きくなりました。

とはいえ、圧縮の効き具合は出力される具体的なHTMLに依るところが大きいのは想像に難くありません。今回のアプリケーションは圧縮が効きやすいように作っています。

また、転送量だけ見るとServer Componentのほうが大きいとはいえ、Server Component版のほうがクライアントで実行されるコンポーネントが少ないため、hydration速度で勝るケースもあるかもしれません（調べたところ今回のアプリケーションではそうでもありませんでしたが）。

筆者としては、Server Componentに寄せることの設計上のメリットが大きいと感じているため、これくらいの転送量増加であれば気にせずServer Componentを使用します。

## 結論

```html
<ruby>推測するな、計測せよ <rt>いつもの</rt> </ruby>
```

[ちなみに、この言葉は元はそういう意味ではないらしいですね。](https://qiita.com/e99h2121/items/e8f899756b21b0414835)

今回の実験に使ったコードはこちらです。

https://github.com/uhyo/rsc-transfer-size-exp

## Q&A

**Q.** ソースを表示したらServer Component版もClient Component版もどちらも1000匹分のHTMLがベタ書きされているんだけど。

**A.** それはSSR結果なのでこの記事の内容とはあまり関係ありません。ソースコード後半の`<script>`内に違いが現れています。