Next12 の React Server Componentsを試してみた

こんにちは。ATOM事業部フロントエンドテックリード兼デザイナーの河原です。

今回は Next12が提供する React Server Components について調べてみました。 サーバサイドでコンポーネント単位でレンダリングするというのが意味不明だったので、気になってました。

React Server Componentsとは

nextjs.org

React Server Components allow us to render everything, including the components themselves, on the server. This is fundamentally different from server-side rendering where you're pre-generating HTML on the server. With Server Components, there's zero client-side JavaScript needed, making page rendering faster. This improves the user experience of your application, pairing the best parts of server-rendering with client-side interactivity. (React Server Componentsを使用すると、対象のコンポーネント自体を含むすべてをサーバでレンダリングすることができます。これは、サーバ上でHTMLを生成する従来のSSRとは根本的に異なります。サーバコンポーネントを使用すると、クライアント側のJavaScriptが不要になり、ページのレンダリングが高速になります。これによりサーバレンダリングの良いところとクライアントサイドのインタラクティブ性を組み合わせて、アプリケーションのUXを向上させることができます。)

執筆時点(2022/01)ではReact本家、Next共に実験的機能として提供されてます。

背景(レンダリング技術の歴史)

WEBサイトのレンダリング技術はここ10年で、昔ながらの SSR(Server Side Rendering) からReactやVueを用いた SPA(Single Page Application) に移行しました。

SPAは 「UXの向上」や「ページ遷移の高速化」など利点がありますが、「初期表示が遅い」「動的なOGP対応が困難」などの欠点もあります。

その欠点を補うべく、NextやNuxtではSPASSRSSG(Static Site Generator)を組み合わせる手法がとられるようになりました。

React Server Componentsではこれまでとは別のアプローチで、SPASSRの良いとこ取りを目指します。

React Server Componentsの仕組み

React Server ComponentsはHTMLをサーバ側で生成する従来のSSRとは根本的に異なります。 サーバ側では仮想DOMの生成までを行います。 サーバコンポーネントのレンダリングの結果(仮想DOM)はHTTPリクエストを介してブラウザに渡り、ブラウザ側でクライアントコンポーネントと合わせてレンダリングを完成させます。

3種類のコンポーネント

React Server Componentsでは次の3種類のコンポーネントが登場します。

  • サーバコンポーネント
    • サーバ(Node)でのみレンダリングされるコンポーネント。ファイル名の末尾が.server.js
    • このコンポーネントで使用するコードはブラウザがダウンロードするJSにはバンドルされない
      • サイズの大きいライブラリも使いやすい
    • DBなどのサーバリソースにアクセス可能
  • クライアントコンポーネント
    • ブラウザでのみレンダリングされるコンポーネント。ファイル名の末尾が.client.js
    • 状態が持てる
    • ブラウザAPIにアクセス可能
    • イベントハンドルが可能
  • ユニバーサルコンポーネント
    • インポート先に応じて、両側で使用およびレンダリング可能なコンポーネント
    • サーバコンポーネントとクライアントコンポーネントの両方の制約(できないこと)を持つ

サーバコンポーネントから他のサーバコンポーネントやクライアントコンポーネントをインポートできます。
クライアントコンポーネントからサーバコンポーネントをインポートすることはできません。
(上記はコンポーネントの親子関係ではなく、ファイルのインポートの親子関係に関する制約)

サーバコンポーネントのレンダリングの結果

通常開発者は意識しませんが、サーバコンポーネントのレンダリングの結果はHTTPリクエストで取得されます。

Request

/rsc?__flight__=1

Response (仮想DOM)

M1:{"id":"./components/client-counter.client.tsx","name":"default","chunks":[]}
J0:["$","main",null,{"children":[["$","h1",null,{"className":"page-title","children":"Example Server Components"}],["$","div",null,{"className":"flex-between","children":[["$","@1",null,{}],["$","div",null,{"className":"example-block","children":[["$","div",null,{"className":"example-title","children":"Server Component"}],["$","div",null,{"suppressHydrationWarning":true,"style":{"textAlign":"center","fontSize":14,"paddingBottom":20},"children":["Rendered at ","2022/01/24 08:36:37.373737"]}],["$","div",null,{"suppressHydrationWarning":true,"className":"description","children":"ãµã¼ãã§ã¬ã³ããªã³ã°"}]]}]]}]]}]

上記のResponseとクライアントサイドの情報をMixして、最終的にクライアントがレンダリングを完成させます。

NextでReact Server Components(α版)を使ってみる

React Server Componentsを有効にする

next.config.js

/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    reactRoot: true,  
    concurrentFeatures: true,
    serverComponents: true,
  },
}

module.exports = nextConfig

Nextはv12以上、Reactはv18以上を使用します。

package.json

  "dependencies": {
    "next": "^12.0.8-canary.11",
    "react": "^18.0.0-rc.0",
    "react-dom": "^18.0.0-rc.0"
  },
ページを作成

pages/rsc.server.tsx

import type { NextPage } from 'next'
import ClientCounter from '../components/client-counter.client' 
import ServerTimer from '../components/server-timer.server'

const RscPage: NextPage = ({}) => {
  return (
    <main>
      <h1 className="page-title">Example Server Components</h1>
      <div className="flex-between">
        <ClientCounter /> 
        <ServerTimer />
      </div>
    </main>
  )
}

export default RscPage

ClientCounterはクライアントコンポーネントです。状態を待ち、イベントをハンドリングします。 このコンポーネントでライブラリを使用すると、バンドルサイズ(通信量)が増えます。

ServerTimerはサーバコンポーネントです。状態を持てず、インベントのハンドリングもできません。 このコンポーネントでライブラリを使用しても、バンドルサイズは増えません。

全てのコードはこちらに置いてます。

まとめ

React Server Componentsを使うことでSPAとSSRの良いとこ取りができるようになります。

  • SSR の良いとこ
    • 初期表示の高速化(バンドルサイズの縮小)
    • サイズを気にせずライブラリが使用できる
    • DBなどのサーバリソースへのアクセス
    • 秘匿したいビジネスルール(計算式など)を隠蔽できる
  • SPAの良いとこ
    • 高い対話性

一方で課題もあります。

  • 制約が多い(現状α版で仕様も固まってないため今後緩和させる可能性はある)
  • 開発難易度が高くなる
  • SSR同様、Nodeサーバが必要となる

技術的に興味深い機能ではありますが、現時点では制約の多さがネックとなり使用したいと思えるシーンが少そうな印象です。

制約が緩和されることを期待して、今後ともウォッチしていきます。

参考

github.com