🎮

React Server Components はウェブ開発を変えるゲームチェンジングな技術である

2021/01/03に公開

去年末に Facebook の人達が出した React Server Components というものが、React 界隈に激震を及ぼしていますが、速報以外でこの技術について言及している国内のブログが見当たらないため、この記事で解説してみます。間違いや分かりづらい部分があればぜひツッコミをお願いします。

React Server Components は、ただのサーバーサイドレンダリングではありません。クライアントサイドレンダリング(SPA)とサーバーサイドレンダリングを、ギアを切り替えずにいいとこ取りする仕組みです。これまでに存在した様々な技術よりも踏み込んで、フロントエンドとバックエンドの境目を曖昧にしてしまうユニバーサルな技術です。

勝手な造語としていうなら「コンポーネント指向ユニバーサルウェブ開発」とでも呼ぶべきものでしょう。

そして、これはただのユニバーサルなだけの仕組みではありません。様々な点でウェブアプリケーションの性能を改善できる仕組みです。

  • 初期レンダリングを高速化する
  • バンドルサイズを抑えてくれる
  • データベースやファイルシステム、APIなどのアクセスも React のコードとして実行でき、なおかつそれらは最適化できる
  • サーバーレンダリングは、いい感じに分割してくれるので再利用しやすい(クライアントでキャッシュが効く)

React Server Components はこれらの性能改善を、あまり面倒なことをせずとも得られるという理想的な技術(になる予定)です。

ただし、この技術はまだβですらありません。

React Server Components は今後の React に取り込まれる予定で RFC(Request for Comments)に PR が投げられてる段階です。雑にいうと「React に Server Componentsっていうアイデアを盛り込みたいんだけど、意見くれよ」という PR を投げて、絶賛議論中というステータスです。

ちなみに React の master ブランチにはすでに必要なものは取り込まれていたり、experimental という形で npm もリリースされてますが。まだ今後の方向性について確定してることはあまりありません。

そのため、この記事で書かれていることは、あくまで最初期のスナップショットのようなものです。将来的にはきっと色々なものが変わるはずです。ただし、根本的なアイデア自体は変わることがないはずです。

(現状の)React Server Components について

Introducing Zero-Bundle-Size React Server Components – React Blog という記事が震源地で、動画を見るのが手っ取り早いです。

Next.js の vercel の人の動画もとても分かりやすいです。

概要

React Server Components では

  • サーバー側でレンダリングするサーバーコンポーネント
  • クライアント側でレンダリングするクライアントコンポーネント
  • 両方でレンダリングするコンポーネント

コンポーネント単位でこれらをファイル名ルールだけで切り替えることができます。App.server.js ならサーバー側でのみ動くサーバーコンポーネントであり、NoteEditor.client.js ならクライアントコンポーネントであり、NotePreview.js は両方から使えるコンポーネントです。

サーバーコンポーネントではサーバー上でのみレンダリングされ、そのときに使われる JavaScript のコードはクライアント側の JS にはバンドルされません。たとえば Hoge.server.js がどれだけ重たい JS を import したとしてもレンダリングされた結果のみがクライアントに配信されて、クライアント側のバンドルされたJSのコードには含まれません。

また、サーバーコンポーネントではNode.jsのあらゆるコードが動くため、データベースやWebAPIなど、あらゆるリソースにアクセス可能です。WebAPI にアクセスする場合ですら、クライアントからアクセスするよりもレイテンシを抑えられますし、memcacheやRedisなんかも活用でき、キャッシュ戦略も自由自在かつ効きやすくなります。

そもそもバックエンドを別途立ち上げる必要性が今後減っていくはずです。もともと Node.js は I/O に強い構造をしているため、ネイティブモジュールを含めたあらゆる Node.js のサーバー向けコードが動く利点は大きいです。

Node.js にとっての弱点である、過度なCPU依存の演算処理などは何かしら工夫が必要かもしれません。

React Server Components を使う限り、もはや SSR or CSR(SPA)という議論は意味を無くします。両方のいいところどりができて、面倒な部分はライブラリやフレームワークが対処してくれて、しかもファイル名を変えるだけで実現でき共存するため、利用者が意識をすることもなくなるでしょう。

ここまで聞くと、Next.js 涙目という感想を抱く人もいるかもしれませんが、実のところ Next.js の領分を犯すものではありません。今後 Next.js はこの技術を取り入れた上で、新しい Next.js を開発していくことになります。

デモを動かしてみてください

https://github.com/reactjs/server-components-demo を是非 clone して手元で動かしてみてください。React Server Components はすでに動く技術です。ちなみに Dockerfile とか docker-compose.yml がありますが、やってることはたんに PostgreSQL を動かしてるだけなので、何かしらの手段で PostgreSQL を動かしさえすれば、あとはいつもどおり Node.js のアプリとして動かすだけで動きます。むしろ、docker-compose だと、Node.js のコードも docker 内部で動作するのでめんどくさいです。

npm i
npm run seed
npm run start

npm run seed は、初期データをセットアップするコマンドです。npm run start で localhost:4000 にアクセスできるようになるはずです。

サーバー上で動くコンポーネントとは?

React Server Components という名前の通り、サーバー上でコンポーネントが処理されるようになります。

デモ用リポジトリの src/ 下には、それぞれ xxxx.server.jsxxxx.client.jsxxxxxx.js というファイル郡があります。

% ls
App.server.js                NoteList.server.js       SidebarNote.client.js
Cache.client.js              NoteListSkeleton.js      SidebarNote.js
EditButton.client.js         NotePreview.js           Spinner.js
LocationContext.client.js    NoteSkeleton.js          TextWithMarkdown.js
Note.server.js               Root.client.js           db.server.js
NoteEditor.client.js         SearchField.client.js    index.client.js

これらのうち xxxx.server.js は、Server 上でのみ処理される React コンポーネントです。

たとえば App.server.js は、以下のような、ただの React ソースです。

// 色々省略
import {Suspense} from 'react';
import Note from './Note.server';
import SearchField from './SearchField.client';
import NoteSkeleton from './NoteSkeleton';

export default function App({selectedId, isEditing, searchText}) {
  return (
        <section className="sidebar-menu" role="menubar">
          <SearchField />
          <EditButton noteId={null}>New</EditButton>
        </section>
        <nav>
          <Suspense fallback={<NoteListSkeleton />}>
            <NoteList searchText={searchText} />
          </Suspense>
        </nav>

この App.server.js はサーバーコンポーネントなのでサーバー上でレンダリングされて、レンダリング結果のみがクライアントに配信されます。

J0:["$","div",null,{"className":"main","children":[["$","section"...

クライアントに配信されるデータというのは、こういったJSONっぽいなにかです。JSX Element のツリーをJSON化したものだと思えばいいでしょう。このツリーは単独では完結せず、このツリーの中でクライアントコンポーネントや、サーバー側でレンダリングされた別の結果なども参照されます。

これによって、サーバー上で解決できる範囲で仮想DOMのツリーとしてレンダリングされ、未解決の部分はクライアント側で解決されたあとに、ReactDOM によってレンダリングが完成します。

  1. xxxx.server.js はサーバー上でのみ動作する React コンポーネントであり、レンダリング結果がクライアント側に特殊な形式で渡される
  2. xxxx.client.js はクライアント上でのみ動作する React コンポーネントだが、サーバーコンポーネントから import することは可能で、サーバー上では未解決のものとして扱われクライアント側でレンダリングされる
  3. クライアント側は配信されたデータをもとにレンダリングを完成させる

ちなみに、処理の大部分は現状での実装に過ぎないので、βや本リリースされる頃に、どういうデータ形式・プロトコルになっているかはまだ未定です。ご注意ください。というか多分変わると思います。

デモはどういう構成なのか?

デモは実際のところ React の最新版とWebackプラグインを活用した、ただの Node.js アプリと、特殊なバンドルをしたクライアント(ブラウザ)向けの JavaScript の組み合わせです。

  • サーバー側は server/api.server.js を起点とした、Node.js で動く Express アプリ
  • クライアント側は scripts/build.js によって Webpack バンドルされたもので、エントリポイントは src/index.client.js
サーバーのエンドポイント 内容
/ ブラウザの起点となるHTMLを配信
/react サーバーサイドレンダリングされた結果を返す
/notes(GETのみ) ノートアプリのデータ参照だが、サーバーからのみアクセスする
/notes(GET以外) ノートアプリの更新系で、クライアントからアクセスして、レンダリング結果を返す
/sleep わざと遅延させるための実験用API(これもサーバーからアクセスする)

クライアントがアクセスするのは基本的には / やバンドルされたJavaScriptのコードや static なリソースと、/react もしくは /notesPOST PUT DELETE メソッドなどです。/react/notes の結果は、先程の JSON っぽいなにかです。

/notesGET/sleep はサーバーからしかアクセスされません。というか、デフォルトのコードではこれらの呼び出しは無効化されています。

server/api.server.jspipeToNodeWritable(React.createElement(ReactApp, props), res, moduleMap); がJSXをもとに React.createElement したものを、例のJSONっぽい何か形式でストリームに書き出す処理ですが、ここらへんの挙動は今後変わる可能性があるだろうから、気にしなくてもいいと思います。

クライアント側向けにバンドルされたJSコードでは、unstable な API を叩きまくって、かなり特殊なことをしています。

  • /react?localtion=..... にアクセスすることで、レンダリングされたもの(JSONっぽい謎のデータ)を受け取る
  • 受け取ったJSONもどきをキャッシュしつつ、react-server-dom-webpack パッケージの createFromFetchを使い、レンダリングに必要な関数を組み立てる
  • 組み立てられたものをレンダリングする

ということをやっています。更新系を処理するときは、react-server-dom-webpack パッケージの createFromReadableStream を使ったり、エンドポイントが /notes になったりします。

基本的には、クライアント側にバンドルされたコードが、能動的にやることはほぼありません。例の JSON もどきを受け取ってレンダリング関数を組み立ててレンダリングするだけです。更新系も、その性質上似たような処理が記述されています。それら以外のクライアントコンポーネントはステートを持つためのものです。

もし真面目にここらへんを追いかけたいのであれば、react のソースの master ブランチの packages/react-server-dom-webpack などを参照すればいいと思います。

ただ、正直、こういった仕組みがリリースまで残るとは考えづらく、何かしらの方法で隠蔽される可能性が高いはずです。

現状では、コンセプトを実証するための実験コードです。

DBとかファイルシステムとか

デモでは react-fs react-pg react-fetch などというパッケージ(これも React リポジトリの master ブランチに含まれる)を使っています。これらのパッケージは、クライアントサイドとサーバーサイドで異なる動きをするラッパーを挟んだ、ファイルシステムやDBを叩くライブラリです。

ただし、現実的には、react-fs react-pg はそれぞれサーバー上でしか動作しないようにしただけの、ファイルシステム・PostgreSQL ラッパーです。

react-fetch はどちらでも動きます。クライアントからどうしても WebAPI にアクセスしたい場合に使うことを前提としているのでしょう。

少し細かい話をすると、これらのパッケージには、ブラウザ向けエントリポイントと、Node.js向けエントリポイントがあり、react-fsreact-pg のブラウザ向けエントリポイントでは throw new Error するようになっています。

throw new Error(
  'This entry point is not yet supported in the browser environment',
);

将来的にはクライアントコンポーネントからアクセスしたときにも、何かしらプロキシー的な動作をする仕掛けを盛り込む可能性もあります。(※個人的にはその方向性はないと思っています)

TypeScript には未対応

根本的な仕掛けとして、.client.js のみをバンドルする、みたいな感じの webpack プラグインが使われているためか、現状では .tsx など TypeScript を動かすことは困難です。プラグインなど様々なところに手を入れる必要があるでしょう。

APIアクセスの多段待ちをなくせる

これは僕も Vercel の人の動画を見て気づいたんですが、APIアクセスの多段待ちがとても簡単に解決できるようになります。

たとえば、多くのウェブサービスでは、認証APIを叩いて、ユーザー情報APIを叩いて、ほげほげAPIを叩いてというふうに、複数の API を叩くことになります。ところがウェブブラウザは1つのサイトに対して最大同時アクセス数は極めて小さな数字に制限されています(制限されていないとDoSになってしまうため)。

認証APIはさすがにどうやっても待たないといけないでしょう。そういったものはどうしようもありませんが、それ以外のリソースは同時にアクセスしても良いと思いませんか?

React Server Components では API 呼び出しをサーバーコンポーネント上に記述することで、ウェブブラウザとは異なり、同時アクセスはサーバーのリソースが許す限り並列に行えます。そして、それらが完了してるかどうか?の面倒な処理も React suspense の仕組みに乗っかることができます。

GraphQL を使えばリソースを一括で取ってこられるでしょう。REST APIでも魔法のような(けど治安を悪化させる)API を生やせば可能でしょう。

React server Components ならそういった特殊な工夫は全く必要ありません。suspense の仕組みに則って、API 呼び出しをサーバーコンポーネントに記述する限り、そういったパズルから開放されます。

あるAPIの結果がないとアクセスしてはいけない次のAPIがある場合も、コンポーネントツリーと Props の指定などを気をつけるだけで実現できます。

僕がどうしてこの技術をとてもクールだと思ったか?(個人の主観に基づきます)

最近は Next.js を中心として、ユニバーサルフレームワークを作るブームがあり、戦国時代に突入しようとしています。blitz などが有名なところです。ところが、それらは割とゴテゴテした構造と仕組みによって作られています。とても多くのライブラリに依存し、多くの工程でビルドされ、多くの仕組みを利用し、覚えなければいけないことも大量か、特定のローカルルールに囲い込まれることになります。

また、これらのフレームワークではユニバーサルとはいえ、基本的にはサーバー側は、Node.js アプリケーションとして記述されることが一般的なため、フロントエンド側とは全く違うルールで記述されます。

React Server Components は、クライアントサイドもサーバーサイドも、React Components として記述しようとしている点がとてもクールです。

React Hooks のときと同様に、エコシステムがゴテゴテしてややこしくなっていきそうなところに、React のコアをいじらなければ実現できない解決方法で、よりシンプルで、いくつもの問題を一気に解決する方法を本家が示してくれたのです。

Next.js も巻き込んで Webpack プラグインの開発をしていますし、ゴテゴテしたフレームワークにちょっとした冷水を浴びせかけることで、よりよいユニバーサルフレームワークを生み出す礎になってくれることでしょう。

僕は、この React Server Components は、React Hooks に続くゲームチェンジングになると確信しています。今回はウェブ開発全域を巻き込んだ大きなものになるでしょう。

Discussion