React サーバーコンポーネントへの50,000行のコード移行前に知っておきたかったこと

 
0
このエントリーをはてなブックマークに追加
Daichi Takayama
Daichi Takayama (高山 大地)

以下の文章は執筆者のDarius Ceplisさんからの許可を得て翻訳したものです。

https://www.mux.com/blog/what-are-react-server-components

Reactのサーバーコンポーネントは、ウェブ開発において非常に重要な役割を果たします。この記事では、最近Muxで行われた大規模なプロジェクト、具体的にはMuxの公式サイトとドキュメントサイトをサーバーコンポーネントに移行した経験について紹介します。このプロジェクトを通じて、サーバーコンポーネントの重要性とその可能性、そしてそれがもたらす価値について深く理解しました。

この記事では、サーバーコンポーネントが何故重要なのか、どのような場面で役立つのか、また、どのような場面では不向きなのかを説明します。また、サーバーコンポーネントの使い方、段階的な導入方法、そして効果的な管理のための高度なパターンについても触れています。この記事を読み終える頃には、Reactのサーバーコンポーネントをどのようにして効果的に使用すべきかについて、より良い理解を得られるでしょう。

Reactサーバーコンポーネントへの道のり

Reactサーバーコンポーネントを深く理解する第一歩は、それが解決しようとする問題点を知ることです。この旅を始めましょう。

かつて、遠く離れた過去に、私たちはPHPなどの技術を使ってサーバー上でウェブサイトを生成していました。この方法は、サーバーがデータを取得し、重い計算処理を行うことで、カスタマイズされた軽いHTMLページをクライアントに提供するのに適していました。

図解1

従来のサーバーサイドレンダリング(SSR)から離れ、より迅速な応答と高度なインタラクティビティを求めた結果、開発者たちはCSRやSPAに目を向けました。このアプローチでは、クライアント(ブラウザ)がレンダリングの処理を担い、全てのレンダリングコードがJavaScriptとしてクライアントに送信されます。

CSR/SPAは一時期、Reactチームによってcreate-react-appを通じてデフォルトのアプローチとして推奨されていました。特に頻繁に更新される高いインタラクティブ性を持つページ(例えばダッシュボード)では、この方法が十分機能すると考えられていました。

しかし、CSR/SPAにはいくつかの問題点が指摘されています。たとえば、JavaScriptを実行しない検索エンジンによるページの読み取りが難しい、サーバー上の秘密情報の管理が困難、また、低性能なデバイスや不安定な接続を持つユーザーが多い(多くの場合、そうである)という点です。
図解2
図解3

そこでサーバーサイドレンダリング(SSR)と静的サイト生成(SSG)が登場します。Next.jsやGatsbyなどのツールはSSRとSSGを活用して、サーバー上でページを生成し、HTMLとJavaScriptの形でクライアントに送ります。この方法では、クライアントはすぐにHTMLを表示でき、JSが読み込まれるとサイトがインタラクティブになります。さらに、検索エンジンがHTMLを読むことができるのも利点です。

図解4

図解5

しかし、SSR/SSGにもまだ解決すべき問題があります。ほとんどの場合、サーバーで生成された全てのJavaScriptをクライアントに送り、クライアントがそれを再実行し、生成されたHTMLと結合する必要があります。これはハイドレーションと呼ばれるプロセスですが、本当に全てのJavaScriptを送り、実行する必要があるのでしょうか?

また、サーバーサイドレンダリングに時間がかかる場合、ユーザーは待たされることになります。これは避けたい問題です。

ここでReactサーバーコンポーネントの重要性が浮かび上がります。この新技術は、上記の問題を解決するために開発されました。

React サーバーコンポーネントとは何か、また、何に適しているのか

React サーバーコンポーネント(RSC)は、その名の通り、クライアントではなくサーバー上で実行されるReactコンポーネントです。しかし、単に「何であるか」よりも「なぜ必要か」が興味深い部分です。RSCは、従来のサーバーサイドレンダリング(SSR)に対していくつかの明確な利点を提供します。

まず、RSCをサポートするフレームワークは、コードがサーバー上でのみ実行されるべきか、またはクライアント上で実行されるべきかを定義する機能を提供します。これはサーバーコンポーネントとクライアントコンポーネントの区別を明確にします。この明確化により、クライアントに送るJavaScriptの量を減らし、バンドルサイズを小さくすることができ、ハイドレーション中の処理も減少します。

図解6

RSCを使うことの2つ目の大きな利点は、サーバーコンポーネントがコンポーネント内から直接データを取得し、そのデータをクライアントにストリーミングできることです。

この新しいデータ取得方法は、Reactでのデータ取得をよりシンプルにします。サーバーコンポーネントは、ノードライブラリを使用したり、fetch関数を使って直接データを取得できます。つまり、ユーザーコンポーネントはユーザーデータを、映画コンポーネントは映画データを取得できます。これにより、複雑なローディング状態の管理や、getServerSidePropsを使用したページレベルでの大量データ取得の必要がなくなります。

また、このアプローチは、データベースの遅い呼び出しによる待ち時間の問題も解決します。準備ができた時点でのみ、遅いコンポーネントをクライアントに送ることができます。これにより、ユーザーは他の部分のサイトを待ち時間中に楽しむことができます。

図解7

さらに、ユーザーのクライアント上のアクション(例えばフォームの送信)に応じてサーバー上でデータを取得する機能もあります。クライアントはデータをサーバーに送り、サーバーはデータの取得やその他の処理を行い、最初のデータと同様にクライアントにレスポンスをストリーミングできます。この2方向のコミュニケーションは技術的にはReactサーバーコンポーネントではありませんが、同じ基盤上に構築されており、密接に関連しています。この話題に関しては、この記事では深く掘り下げておりませんので、こちらの英語記事をご参照ください。

React サーバーコンポーネントが向いていないのはどのような場合か?

ここまで、React サーバーコンポーネント(RSC)についてかなり肯定的な面を書いてきました。もしRSCがCSRやSSRよりも優れているなら、なぜそれを使わないのでしょうか? 私もそのように疑問に思いしたが、この記事のタイトルが示唆するよう、より険しい方法で学びました。実際にはいくつかの落とし穴があるのです。ここでは、Reactサーバーコンポーネントへの移行に最も時間を費やした3つの点を挙げます。

1.CSS-in-JSの非互換性:

現在の段階では、CSS-in-JSはサーバーコンポーネントで動作しません。これは、CSS-in-JSを用いているプロジェクトにとって大きな問題となります。例えば、styled-componentsからTailwind CSSへの移行は、サーバーコンポーネントへの変更において大きな作業となるでしょう。したがって、CSS-in-JSに依存している場合、他のソリューションへの移行や、これを機に良い代替案を検討する必要があるかもしれません。

2.Reactコンテキストの非対応:

React サーバーコンポーネントでは、Reactコンテキストを使用することはできません。この機能はクライアントコンポーネントでのみ利用可能です。サーバーコンポーネント間でpropsを使わずにデータを共有したい場合、代わりに通常のモジュールを使うことになるでしょう。

特に問題となるのは、Reactアプリケーションの特定のサブツリーに限定したデータを扱いたい場合です。サーバーコンポーネントでは、このようなデータの制限を行う効果的なメカニズムが存在しません。これは、例えばドキュメントサイトでは大きな問題ではありませんでしたが、マーケティングサイトのような場合には大きな課題となる可能性があります。

例えば、マーケティングサイトではテーマを共有するエリアが存在し、これらのエリアでコンテキストを使用してテーマの状態を共有することが一般的です。しかし、サーバーコンポーネントではコンテキストが利用できないため、代わりにCSSカスタムプロパティを使用することでこの問題を解決しました。これはスタイリングの問題であり、データの問題ではないため、おそらくより適切な方法であると考えられます。しかし、このような解決策が適用できない場合もあり、他の開発者にとっては難しい課題となるかもしれません。

この説明は、Reactサーバーコンポーネントの制約と、それに対応するためのクリエイティブな解決策を求める必要性を示しています。特に、アプリケーションの特定部分に特定のデータやスタイルを限定する場合、従来の方法とは異なるアプローチが求められることがあります。

3.複雑性の増加:

RSCは、コードの実行場所やデータ取得の方法についてより多くの柔軟性を提供しますが、この柔軟性は複雑性を伴います。Joel Spolskyが指摘するように、どのようなツールもこの複雑性を完全には隠蔽できません(「The Law of Leaky Abstractions」参照)。そのため、開発者はこの複雑性を理解し、それに対処し、他の開発者とコミュニケーションを取る必要があります。

新しい開発者がコードベースに取り組むたびに、「サーバー上で実行されているのは何か?クライアント上で実行されているのは何か?」という質問が頻繁に発生します。プルリクエストでは、誤ってまたは不必要にクライアントに送られたコードに関するフィードバックがあり、開発者はコンソールログを追加してサーバーかクライアントのどちらがログを取るかを確認します。また、データキャッシングの複雑さについても言及されています。

これらの課題は、実践と信頼できるパターンを通じて改善されています。したがって、React サーバーコンポーネントの使用方法、段階的な移行方法、複雑なことを行う方法について議論することが重要です。特に、理解しにくいスパゲッティコードを作成せずに、これらの複雑なシステムをどのように効果的に管理するかについての考え方や方法論が求められます。

Reactサーバーコンポーネントの使用方法

React サーバーコンポーネント(RSC)を導入する際の基本的なアプローチについて説明します。

<aside>
💡 この記事が書かれた時点では、React サーバーコンポーネントの唯一の本番環境での実装例は、Next.js 13の新しいアプリケーションディレクトリ構造によるものです。Next.js 13はRSCをサポートしており、Reactの新しい機能をフルに活用しています。もちろん、技術に精通している開発者は独自のRSCフレームワークを構築することも可能ですが、多くの場合、既存のフレームワークを使用する方が効率的であり、サポートも充実しています。

</aside>

サーバーコンポーネント

サーバーコンポーネントのメンタルモデルは複雑かもしれませんが、構文は驚くほどシンプルです。Next.js 13の新しいアプリディレクトリでは、デフォルトで書かれたコンポーネントはサーバーコンポーネントとして動作します。これは、ページのコードがクライアントに送信されないことを意味します。以下のような基本的なサーバーコンポーネントは、単にHTMLとしてレンダリングされますが、クライアントにはJavaScriptコードとして送信されません。

基本的なサーバーコンポーネント

function Description() {
  return (
    <p>
      このコードはクライアントに送信されません。HTMLだけです!
    </p>
  )
}

async関数を使用することで、サーバーコンポーネントからデータをフェッチすることが可能です。この方法では、サーバーサイドでデータを取得し、その結果をコンポーネントに反映させます。

データ取得付きのサーバーコンポーネント

async function getVideo(id) {
  const res = await fetch(`https://api.example.com/videos/${id}`)
  return res.json()
}

async function Description({ videoId }) {
  const video = await getVideo(videoId)
  return <p>{video.description}</p>
}

RSCの真の力を解き放つための最後の要素は、遅いデータ取得によって待たされたくない場合、サーバーコンポーネントをReact.Suspenseでラップすることです。Reactはクライアントにローディングの代替テキストを表示し、サーバーがデータ取得を完了すると、その結果をクライアントにストリームします。その後、クライアントはローディングの代替テキストを完全なコンポーネントに置き換えることができます。

以下の例では、クライアント側からは「コメントをロード中」と「関連動画をロード中」と見えます。サーバーがコメントの取得を完了すると、<Comments />コンポーネントをレンダリングし、レンダリングされたコンポーネントをクライアントにストリームします。関連動画についても同様です。

データ取得とストリーミング付きのサーバーコンポーネント

import { Suspense }  from 'react'

async function VideoSidebar({ videoId }) {
  return (
    <>
      <Suspense fallback={<p>loading comments...</p>}>
        <Comments videoId={videoId} />
      </Suspense>
      <Suspense fallback={<p>loading related videos...</p>}>
        <RelatedVideos videoId={videoId} />
      </Suspense>
    </>
  )
}

React.Suspenseを使用することで、データが準備されるまでの待ち時間を効果的に管理し、ユーザーに快適なインタラクションを提供できます。また、選択的ハイドレーションという高度なテクニックを使うことで、アプリケーションの特定の部分を優先的にハイドレートすることも可能になります。これらのテクニックは、ユーザーのインタラクションに応じてアプリの特定の部分を効率的にロードし、優れたユーザーエクスペリエンスを提供するために重要な役割を果たします。

クライアントコンポーネント

クライアントコンポーネントは、ユーザーインタラクション(例えば、クリックイベントのリスニング)や状態管理(useStateの使用)に対応するために必要です。

クライアントコンポーネントを定義する最も基本的な方法は、ファイルの上部に "use client" ディレクティブを追加することです。これにより、そのモジュールはクライアントに送られ、クライアントサイドの操作が可能になります。

基本的なクライアントコンポーネント

"use client"
import { useState } from 'react'

function Counter() {
  const [count, setCount] = useState(0)
  const increment = () => setCount(count + 1)

  return (
    <button onClick={increment}>
      カウントは {count} です
    </button>
  )
}

もう一つの重要な点は、"use client" を使用することで、そのコンポーネントだけでなく、それがインポートするすべてのコンポーネントもクライアントに送られるということです。この挙動は、クライアントとサーバーのコードの分離を助け、必要なコンポーネントだけがクライアントに送信されるようにします。

ただし、サーバーコンポーネントがクライアントコンポーネントの子供になることは可能ですが、その逆はできません。後述するように、この関係性の詳細な理解が重要になります。

これらの原則と機能を理解することで、Reactのサーバーとクライアントのコンポーネントを効果的に活用し、高性能かつインタラクティブなウェブアプリケーションを構築することができます。

クライアントコンポーネントをサポートしていないライブラリの場合はどうするか?

2つ目の方法を活用してよくある問題を解決することができます。RSCをサポートしていないライブラリを使用する場合、"use client" ディレクティブを使って、そのライブラリをクライアントコンポーネントとして明示的に指定することができます。これにより、ライブラリはクライアントに送られ、クライアントサイドで動作するようになります。

ライブラリをクライアントコンポーネントに変換する

"use client"

// このライブラリがクライアントコンポーネントでインポートされるため、
// それもクライアントコンポーネントになります
import MuxPlayer from "@mux/mux-player-react"

function ClientMuxPlayer(props) {
  return <MuxPlayer {...props} />
}

図解9

クライアントコンポーネントを使用するタイミング

それでは要約してみましょう。

サーバーコンポーネントは、データの取得やコストのかかるコードの実行に適しています。これらのコンポーネントは、クライアントに送信する必要がない、あるいは送信したくない作業に最適です。例えば、ブログ投稿のテキストのレンダリングや、コードブロックのシンタックスハイライトなどが挙げられます。クライアントバンドルを肥大化させないためにも、可能な限りサーバーコンポーネントとしてコードを残すのが望ましいです。

クライアントコンポーネントは、既に親しんでいる通常のReactの形式です。サーバーサイドでレンダリングされた後、クライアントに送信され、そこでハイドレーションと実行が行われます。ユーザー入力への反応や時間とともに状態が変化するような場合には、クライアントコンポーネントが適しています。

アプリケーション全体がクライアントコンポーネントから成る場合、従来のサーバーサイドレンダリング(SSR)フレームワークと同じように動作します。そのため、アプリケーションを一気にサーバーコンポーネントに変換する必要はありません。むしろ、最も利益を得られる部分から段階的にサーバーコンポーネントを採用していくのが良いでしょう。

実際のコードベースでReactサーバーコンポーネントを段階的に導入する方法

「面白いけど…かなりの作業のようだし、自分には全てのコードベースを書き換える時間がなんてありませんよ。」と言う方も多いのではないでしょうか?

実は、全てを書き換える必要はありません。そのためにこの記事を書いています。私たちがほとんどのコードをサーバーコンポーネントに移行するために使用した3ステップのプレイブックは以下の通りです:

  1. アプリのルートに「use client」ディレクティブを追加する
  2. レンダリングツリーでできるだけ下にディレクティブを移動する
  3. パフォーマンスの問題が生じたときに高度なパターンを採用する

それを詳しく見ていきましょう。

1. アプリのルートに「use client」ディレクティブを追加する

ただそれだけです。Next.js 13を使用している場合は、トップレベルのpage.tsxに行き、「use client」を最上部に追加します。ページは以前と同じように機能しますが、これでサーバーコンポーネントの世界に飛び込む準備が整いました!

"use client"

export default function App() {
  <>
    <Player />
    <Title />
  </>
}

図解10

サーバーサイドのデータフェッチ? クライアントコンポーネントからはできませんので、サーバーコンポーネントを追加します。サーバーコンポーネントをクライアントコンポーネントの親として追加しましょう。そのサーバーコンポーネントがデータフェッチを行い、ページにデータを渡します。以下がその様子です:

/**
  * ここで行っていることは、サーバー上でデータを取得し、
  * そのデータをクライアントコンポーネントに渡すことです。
  */
import VideoPageClient from './page.client.jsx'

// これは以前 getServerSideProps でした
async function fetchData() {
  const res = await fetch('https://api.example.com')
  return await res.json()
}

export default async function FetchData() {
  const data = await fetchData()
  {/* ページの内容をこのクライアントコンポーネントに移動しました */}
  const <VideoPageClient data={data} />
}

export default Page

/**
  * データフェッチを除いて、アプリ全体はここに存在できます。
  */
"use client"

export default function App({ data }) {
  <>
    <Player videoId={data.videoId} />
    <Title content={data.title} />
  </>
}

図解11

2. レンダリングツリーでできるだけ下にディレクティブを移動する

次に、「use client」ディレクティブをトップレベルのコンポーネントからそれぞれの子コンポーネントに移動します。この例では、<Client /> コンポーネントから <Player /> と <Title /> コンポーネントに移動させます。

// video/Player.jsx

"use client"
import MuxPlayer from "@mux/mux-player-react"

function Player({ videoId }) {
  return <MuxPlayer streamType="on-demand" playbackId={videoId} />
}
// video/Title.jsx

"use client"

function Title({ content }) {
  return <h1>{content}</h1>
}

図解12

これを繰り返します。ただし… <Player /> も <Title /> も「use client」ディレクティブを押し込める子コンポーネントを持っていないため、取り除きましょう!

<Title /> は問題ありません。なぜなら <Title /> はクライアントサイドのコードを必要とせず、純粋なHTMLとして送られることができるからです。一方で、<Player /> はエラーを投げます。
図解13

では、エラー対処のために <Player /> コンポーネントに「use client」を復元し、これで一日の仕事を終えましょう。

図解14

意外と簡単です。アプリをサーバーコンポーネントに移行しました。これから新しいコンポーネントを追加したり、古いものをリファクタリングしたりする際には、サーバーコンポーネントを念頭に置いて書くことができます。そして、<Title /> を送信しないことで少しバンドルサイズが節約できました!

3. パフォーマンスの問題が生じたときに高度なパターンを採用する

ステップ1と2でほとんどの場合は十分です。しかし、変換プロセスにおいて、パフォーマンスの問題が発生した場合、追加の最適化でさらに利点を得ることができます。

例えば、私たちのドキュメントサイトをRSCに移行する際には、さらに大きな利益を得るために2つのパターンを利用しました。

  1. 重要なサーバーコンポーネントをSuspenseでラップする:
    遅いデータフェッチをストリーミングするために、主要なサーバーコンポーネントをReact.Suspenseでラップします。例えば、ドキュメントサイトをRSCに移行した際、全アプリが静的に生成されている一方で、CMSから取得するチェンジログサイドバーだけが動的でした。サイドバーをSuspenseでラップすることで、アプリの残りの部分がCMSフェッチを待つ必要がなくなります。さらに、Next.js 13のloading.js規約を利用しました。これは内部的にSuspense/ストリーミングを使用しています。
  2. クライアントとサーバーコンポーネントの創造的な再配置:
    大きなライブラリ(例えば、シンタックスハイライトライブラリのPrism)をサーバー側に留めるために、クライアントとサーバーコンポーネントを創造的に再配置します。このような再配置により、パフォーマンスを向上させ、クライアント側のバンドルサイズを抑えることができます。

高度なパターンについて

クライアントとサーバーコンポーネントの混合方法

クライアントコンポーネントからインポートされるすべてのコンポーネントは、自動的にクライアントコンポーネントとして扱われます。この点は、クライアントとサーバーコンポーネントをどのように組み合わせるかを理解する上で重要です。しかし、サーバーコンポーネントをクライアントコンポーネントの子として組み入れる場合、少し異なるアプローチが必要になります。

具体的には、サーバーコンポーネントを直接インポートするのではなく、子要素やプロパティとしてクライアントコンポーネントに組み込む方法があります。サーバー上でレンダリングされたサーバーコンポーネントは、シリアライズされてクライアントコンポーネントに渡される形になります。このプロセスは、初めは理解しにくいかもしれませんが、実際に試してみることで徐々に慣れていきます。

以下に、サーバーコンポーネントとクライアントコンポーネントの組み合わせ方について、正しい方法と誤った方法の例を示します。これにより、どのようにこれらのコンポーネントを効果的に混在させるかがより明確になります。

クライアントとサーバーコンポーネントを混ぜる間違った方法

"use client"

// クライアントコンポーネントからインポートされたサーバーコンポーネントは間違い
import ServerComponentB from './ServerComponentB.js'

function ClientComponent() {
  return (
    <div>
      <button onClick={onClickFunction}>Button</button>
      {/* このサーバーコンポーネントもクライアントコンポーネントになる */}
      <ServerComponentB />
    </div>
  )
}

図解15

クライアントコンポーネントでサーバーコンポーネントをインポートすることにより、サーバーコンポーネントをクライアントに送ってしまいます。これを正しく行うためには、最も近いサーバーコンポーネント(この場合はServerPage)に移動し、そこで作業を行う必要があります。

クライアントとサーバーコンポーネントを混ぜる正しい方法

import ClientComponent from './ClientComponent.js'
import ServerComponentB from './ServerComponentB.js'

// サーバーコンポーネントを子要素として渡す
function ServerComponentA() {
  return (
    <ClientComponent>
      <ServerComponentB />
    </ClientComponent>
  )
}

// サーバーコンポーネントをプロップとして渡す
function ServerPage() {
  return (
    <ClientComponent content={<ServerComponentB />} />
  )
}

図解16

ファイルの半分をサーバーコンポーネント、半分をクライアントコンポーネントにすることはできるか?

できません。 しかし、コンポーネントの機能の一部をサーバーに残したい場合に私たちがよく使用するパターンがあります。たとえば、<CodeBlock /> コンポーネントを作成する場合、大きなライブラリを送信する必要がないようにシンタックスハイライトをサーバーに残したいとしましょう。ユーザーが複数のコード例を切り替えることができるようにクライアント機能も必要です。まず、コンポーネントを二つの部分に分割します:CodeBlock.server.js と CodeBlock.client.js。前者は後者をインポートします(名前は何でも良いですが、私たちは .server と .client を使って区別しています)。

// filename: components/CodeBlock/CodeBlock.server.js
import Highlight from 'expensive-library'
import ClientCodeBlock from './CodeBlock.client.js'
import { example0, example1, example2 } from './examples.js'

function ServerCodeBlock() {
  return (
    <ClientCodeBlock
      // because we're passing these as props, they remain server-only
      renderedExamples={[
        <Highlight code={example0.code} language={example0.language} />,
        <Highlight code={example1.code} language={example1.language} />,
        <Highlight code={example2.code} language={example2.language} />
      ]}
    >
  )
}

export default ServerCodeBlock
"use client"
import { useState } from 'react'

function ClientCodeBlock({ renderedExamples }) {
  // because we need to react to state and onClick listeners,
  // this must be a Client Component
  const [currentExample, setCurrentExample] = useState(1)

  return (
    <>
      <button onClick={() => setCurrentExample(0)}>Example 1</button>
      <button onClick={() => setCurrentExample(1)}>Example 2</button>
      <button onClick={() => setCurrentExample(2)}>Example 3</button>
      { renderedExamples[currentExample] }
    </>
  )
}

export default ClientCodeBlock

これら2つのコンポーネントができたら、CodeBlockというフォルダにこれらのファイルを入れ、このようなindex.jsファイルを追加して、使いやすくしましょう。

// components/CodeBlock/index.js

export { default } from './CodeBlock.server.js'

これで、どんなユーザーも ‘components/CodeBlock.js’ からCodeBlockをインポートでき、クライアントとサーバーコンポーネントは透明に保たれます。

自分のコードがサーバー上で実行されているかを確認する方法

開発初期には、どのコンポーネントがサーバー上で動作しているかを判断するために、単純に console.log をコードに追加し、そのログがサーバーやウェブブラウザーから出力されるかをチェックしていました。これは始めのうちは有効な方法でしたが、より良い方法が見つかりました。

サーバーコンポーネントがクライアントのバンドルに含まれないことを確実にしたい場合、server-onlyパッケージを利用することができます。これは、大きなライブラリや秘密鍵が意図しない場所に渡らないようにするために特に便利です。Next.jsを使用している場合、環境変数が誤ってクライアントに送信されることはないため、Next.jsはこの点で保護を提供します

server-onlyを使用することには、もう一つの微妙ですが意味のある利点があります。それは、可読性と保守性です。ファイルの先頭にserver-onlyがあると、保守者はそのファイルがどこで実行されているのかを正確に知ることができ、コンポーネントツリー全体の完全なメンタルモデルを常に頭に描く必要がなくなります。

このようにして、開発者はコードの実行場所をより明確にし、大規模なアプリケーションの管理や保守を容易にすることができます。また、サーバーコンポーネントとクライアントコンポーネントの区分をより明瞭にし、アプリケーションのパフォーマンス最適化に貢献することも可能になります。

結局、Reactサーバーコンポーネントは採用すべきなのか

Reactサーバーコンポーネント(RSC)を採用する際には、それに伴うコストと複雑性を理解し、慎重に検討する必要があります。RSCの導入は無料ではなく、CSS-in-JSやReact Contextのような特有の問題だけでなく、以下のような追加の複雑性をもたらします。

  • サーバーとクライアントで何が実行されているのかを理解すること。
  • ハイドレーションの理解
  • インフラストラクチャのコスト。
  • クライアントとサーバーコンポーネントを混在させる際のコード複雑性の管理。

これらの複雑性は、バグの潜入の可能性を増やし、コードの保守性を低下させるリスクがあります。フレームワークはこれらの複雑性を軽減しますが、完全には取り除くことはできません。

RSCを採用するかどうかを決定する際には、これらのコストを小さなバンドルサイズや高速な実行速度といった利点と天秤にかける必要があります。これらの利点は、特にSEOにとって重要な要素です。また、複雑なデータ集約型のサイトを最適化するための高度なデータローディングパターンを使用することもできます。

Jeff Escalanteは、Reactathonの講演で、RSCを採用するかどうかの判断に関してこの問題を的確に図解しています。

図解17

チームがこれらのメンタルオーバーヘッドを受け入れ、パフォーマンスの利点が価値があると判断した場合、RSCはあなたのプロジェクトに適しているかもしれません。

info-outline

お知らせ

K.DEVは株式会社KDOTにより運営されています。記事の内容や会社でのITに関わる一般的なご相談に専門の社員がお答えしております。ぜひお気軽にご連絡ください。