Next.js v13でサーバーコンポーネントが導入されて以降、App Routerを用いた開発では、あるコードがサーバー側・クライアント側のどちらで実行されるのかを意識する必要が出てきました。この境界が識別しづらいことは、開発者にとって新たな課題となっていると思います。
実行される環境を見誤ると、クライアント専用のAPIをサーバーコンポーネントで呼び出してしまったり、反対にサーバーコンポーネントのみで行いたい処理をクライアントサイドへ露呈してまいセキュリティーリスクへ繋がったり、またバンドルサイズが気づいたら想定をはるかに上回る大きさへ膨れ上がっているかもしれません。
この記事ではこの課題に対して、各コンポーネントのファイルがサーバーコンポーネント・クライアントコンポーネント・もしくは両方になり得るかをVS CodeのUI上で可視化することにより課題の解消を目指した取り組みを紹介します。
サーバーコンポーネントとクライアントコンポーネントの境界
まず改めてサーバーコンポーネントとクライアントコンポーネントとの違いについて簡単に整理してみます。
サーバーコンポーネントとクライアントコンポーネントの違い・役割
Next.jsのApp Routerでは、コンポーネントは「サーバーコンポーネント」と「クライアントコンポーネント」の2種類に分かれます。
サーバーコンポーネント、クライアントコンポーネントという考え方自体は、ReactのServer Componentsの仕組みに基づいており、Next.js固有のものではなく、React Server Componentsをサポートしているバンドラーやフレームワークに共通するコンポーネント種別です。
種類 | バンドルへの含まれ方 | 実行環境・特徴 | 利用可能なAPI |
---|---|---|---|
サーバーコンポーネント | クライアントのJavaScriptバンドルに含まれない | ・バンドル前にサーバー側で実行され、HTMLとして出力される ・DBアクセスや認証などサーバーサイド処理が可能 |
ブラウザAPIやクライアント専用のReactフック(useState , useEffect 等)は利用不可(ただしuseId など一部のフックは利用可能) |
クライアントコンポーネント | クライアントのJavaScriptバンドルに含まれる | ・初回はSSRでHTMLが生成されるが、クライアント側でハイドレーションされて動作 ・ブラウザー上のインタラクションやブラウザー固有のAPIを利用可能 |
ブラウザAPIやクライアント専用のReactフック(useState , useEffect 等)が利用可能 |
コンポーネントがサーバーコンポーネントかクライアントコンポーネントかはどのように決まるか
コンポーネントがサーバーコンポーネントかクライアントコンポーネントかを決定する仕組みとして、use client
ディレクティブがあります。
基本的にこのディレクティブを境界としてコンポーネントがサーバーコンポーネントかクライアントコンポーネントかは決定されます。
ファイルのトップに ‘use client’ を加えることで、当該モジュールとそれが連動してインポートしている依存モジュールがクライアントコードであるとマークします。
いくつかパターンがありますが、それを表にまとめてみると以下のようになります。
親コンポーネントの種類 | 子コンポーネントのuse client 有無 |
子コンポーネントとしての扱い |
---|---|---|
サーバーコンポーネント | ❌ | サーバーコンポーネント |
サーバーコンポーネント | ✅ | クライアントコンポーネント |
クライアントコンポーネント | ❌ | クライアントコンポーネント |
クライアントコンポーネント | ✅ | クライアントコンポーネント |
クライアントコンポーネント(children 経由) |
❌ | サーバーコンポーネント(children として渡す場合は親の影響を受けない) |
このように、親コンポーネントやuse client
ディレクティブの有無、children
として渡すかどうかによって、同じ子コンポーネントでもサーバー・クライアントのどちらにもなり得ることが分かります。
さらに、複数の親コンポーネントから利用される場合、ある親コンポーネントのもとではサーバーコンポーネントとして、別の親コンポーネントのもとではクライアントコンポーネントとして動作するケースもあります。つまり、同じコンポーネントでもレンダーツリー上の場所によってサーバー・クライアント両方の役割を持ち得る、ということです。
開発効率への影響
use client
ディレクティブが記述されたコンポーネントは、クライアントコンポーネントであることが一目で分かります。しかし、ディレクティブがないコンポーネントについては、どのコンポーネントから呼び出されているのかを一つずつ確認しない限り、それがサーバーで実行されるのか、クライアントで実行されるのかを判別することはできません。
このような仕組みのため、プロジェクトの規模が大きくなりコンポーネント数や階層が増えていくと、今開いているファイルがサーバーコンポーネントかクライアントコンポーネントかを即座に判断するのが難しくなってきます。
特に、複数の親コンポーネントから利用される場合や、設計上の意図が明確にドキュメント化されていない場合には、どの環境で実行されるのか把握する必要があります。そのため、依存関係を一つずつ辿ることが求められます。
その結果、開発効率の低下やミスの温床となりがちです。
また、設計段階でuse client
ディレクティブを適切に記述し、サーバーとクライアントの境界を明確に決めていたとしても、記述漏れがあれば判別を誤る可能性もあります。
何気なくuseState
を用いて実装し、動作確認の段階で以下のようなエラーが出力されることで、そのコンポーネントの実行環境を見誤っていたことに気づく、という経験をした開発者も多いのではないでしょうか。
Error: × You're importing a component that needs `useState`. This React hook only works in a client component. To fix, mark the file (or its parent) with the `"use client"` directive.
このエラーについては、少し古いものですがStack Overflowでも同様の質問が一定の注目を集めており、多くの開発者がこの問題に直面し関心を寄せていると読み取れます。
反対にクライアントコンポーネントとして扱われるレンダーツリーにおいて非同期コンポーネントをレンダーしようとすれば、以下のようにサポートされてないことがエラーとして出力されます。
is an async Client Component. Only Server Components can be async at the moment. This error is often caused by accidentally adding `'use client'` to a module that was originally written for the server.
Lintで防げないのか
Lintで防ぐことはできないのかという疑問が浮かぶかもしれませんが、Next.js公式のESLintプラグインでも、こうした問題を包括的に防ぐことは現時点では難しい状況です。
例えば、@next/next/no-async-client-component
というルールは存在しますが、これはuse client
が記述されているファイル自体にしか適用されません。また、2025年6月現在の最新バージョン(v15.3.3)では、このルールがdefault export
のものしか検知できません。
このように、サーバーコンポーネントの登場によるパラダイムシフトは、パフォーマンスやセキュリティー、開発体験などの面で良い変化がある一方で、その変化への対応に苦慮する部分もあると思います。
VS Code上でサーバー・クライアントコンポーネントを可視化する
上述の通りコンポーネントの実行環境を正確に判別することは少し難しい部分がありますが、一方で、このコンポーネントの実行環境が決定される条件自体は、こちらも上述の通りReactの仕様として明確に定義されています。
そこで、明確に決まっているのであればAST(抽象構文木)を用いれば、この条件をプログラム的に判定することが可能であろうと気づきました。
たとえば、各ファイルの先頭にuse client
があるかどうかや、どのコンポーネントがどこからインポートされているかといった情報は、ASTを解析することで自動的に抽出できます。そこで、ts-morphを活用してReactアプリケーションのレンダーツリーの依存関係を構造化すれば、サーバーコンポーネントとクライアントコンポーネントの境界を自動的に可視化できるのではないかと考えました。
ts-morphは、TypeScriptのCompiler APIをラップしたTypeScriptのASTを操作できるライブラリーです。TypeScriptのコードをプログラムから解析・編集するためのAPIを提供しており、型情報や依存関係の取得、ファイル構造の探索などを柔軟に行うことができます。
このts-morphを使えば、各ファイルの先頭にuse client
ディレクティブがあるかどうかを判定したり、どのコンポーネントがどこからインポートされているかといった依存関係を静的に解析できると考えました。
つまり、プロジェクト全体の依存関係をグラフとして構築し、use client
ディレクティブの有無をもとにサーバーコンポーネント・クライアントコンポーネントの境界を可視化できるのでは?というアイディアです。
コンポーネントの実行環境を見誤る場合、実際にブラウザーで動作させてみて初めてエラーに気づくことが多く、そのタイミングで手戻りが発生する状況でした。
たとえば、サーバーコンポーネントでuseState
を使ってしまい、ビルドや実行時にエラーが出て初めて問題に気づく、といったケースです。
こうした問題に、もっと早い段階でフィードバックを受け取れる仕組みがあれば、未然に防げるのではないかと感じました。
ts-morphによる依存グラフの構築によって、コンポーネントの実行環境をエディター上で早期に可視化し、フィードバックを得られるのではないかと考えました。
実際に、サーバー・クライアントコンポーネントの境界をリアルタイムに把握できれば、これまでブラウザーで動作確認をして初めて気づいていた問題も、実装段階で事前に解消できるはずです。
その発想から、普段利用しているVS Codeの拡張機能として実現することにしました。
Next.js Component Boundary Visualizer
上述の背景があり、TypeScriptの静的解析ライブラリであるts-morphを活用し、VS Code上でコンポーネントの種別を可視化する拡張機能を実装しました。
作った拡張機能は、以下のVisual Studio MarketplaceのURLで公開してあります。
この拡張機能ではエクスプローラー内のファイル項目やエディタータブに、そのファイルのコンポーネントがクライアントコンポーネントであれば「⚡️」を、サーバー・クライアントコンポーネントの両方であれば「♾️」のアイコンを表示して、今実装に取り組んでいるファイルがどの実行環境のものかを一目瞭然となるようにしてみました。
ファイルの更新があればそのファイルと依存関係を検知して、実装を進めていく上でコンポーネントの種別に変化があってもリアルタイムにアイコンの状態を追従します。
なお、デフォルトがサーバーコンポーネントなので、サーバーコンポーネントに対しては特にアイコンを表示していません。
この拡張機能によって、これまでは実装の区切りがついてブラウザーで動作確認をしようとたら実行環境を見誤っていることに気づいていたのが、実装をしている最中にエディター上で実行環境を知ることができ、早期に実装を見直すべきであると気づけます。
例えば、useState
を使おうと思ったけど、このファイルは今何もアイコンが表示されていないので、どこかでuse client
ディレクティブを追加する必要があるとすぐに気づけます。また、「このファイルでheaders()
を使ってヘッダー情報を参照しようと思ったけど、⚡️のアイコンがついているからヘッダー情報の取り方を考え直さないといけないな」といった判断も早い段階で可能になります。
拡張機能の開発
拡張機能の開発は、基本的にGithub Copilot Agentをドライバーとして、いわゆるバイブコーディングで進めました。
大雑把なイメージとして、恐らくts-morphであればプロジェクトの依存関係のグラフ化を行なって、use client
ディレクティブの存在チェックを行えるであろうと思っていたので、Github Copilot Agentとそのようなやりとりを最初に行ないました。
幾度か自然言語でやりとりして、ほとんど自分自身でコーディングすることなく、出来上がった依存グラフを生成する実装が以下のクラスです。
VS Codeでアイコン表示する上では、このクラスの.build
の結果をVS CodeのAPIで利用しているだけの単純な作りです。
この依存グラフ生成の実装において悩ましいのはプロジェクトの規模が大きくなるとパフォーマンス懸念が大きいことです。
プロジェクトの全ての.ts
および.tsx
のファイルを対象に依存グラフを生成するため、その対象ファイルが増えるほど処理コストが大きくという点でパフォーマンス上の影響が出やすくなります。
そのためts-morphのインスタンス生成時にファイル追加をスキップ(skipAddingFilesFromTsConfig
をtrue
に)し、後からthis.project.addSourceFilesAtPaths(glob)
として対象ファイルを最低限必要なものに絞り込んでいます。
対象ファイルの絞り込みはパフォーマンス上必要でもあるし、また、Storybookのstoryを記載する*.stories.tsx
のようなコンポーネントをimport
する類であればサーバー・クライアントの実行環境の判定に誤った影響を与えてしまうので、いずれにせよ対象ファイルは指定する必要があります。
また、ファイルの変更や削除時、依存グラフもその変更内容に合わせて更新が必要になりますが、ここでは差分更新してキャッシュを有効活用することでパフォーマンス改善を図っています。
必要最低限のファイルに絞って更新処理を行うことでパフォーマンス上の影響を抑えたく、変更があったファイルそのものと、その依存関係となるファイルに絞って依存グラフを再構築して、サーバー・クライアントコンポーネントの判定を更新しています。
まとめ
今回取り組んだ拡張機能の開発は、サーバー・クライアントコンポーネントいずれの実行環境で動くものかという点に関して、より早い段階でフィードバックを得られるようにするものでした。
単純にアイコンを表示するだけでなく、ASTを活用することで実行環境においてサポートされていないAPIの利用時にエラーを表示するなど、さらなる機能拡張の可能性も考えられると思います。
一方で、特に大規模なプロジェクトではパフォーマンス面での課題が残っており、今後も改善が必要だと感じています。
Views: 0