ベンチマーク
English | 日本語
乖離したデザインシステムの上で画面を組むために、エージェントは何を知っていればよいのか。 そしてそれを運ぶ最小のものは何か。1 つの画面、4 つの UI ライブラリ、3 つのホスト規模。 比較ごとの変数は「エージェントに何を見せるか」の 1 つだけとしました。 計測した実装は 68 です。2026-08-13/14 に計測、各セル 1 回、繰り返しなし。
ハーネス(ホスト、仕様、スクリプト、アームのプロンプト、生成された全画面)は別リポジトリとして公開予定です。 数値は方法への敵対的レビューが採点の欠陥を 2 件見つけたあとに一度訂正しており、訂正内容とレビューの指摘はハーネスに同梱します。
セットアップ
20 コンポーネントの合成プロダクト層(SNS タイムライン相当。投稿カード、コンポーザー、タブ、パネル)を、共通の API 契約に従ってライブラリごとに 1 回ずつ、計 4 回実装しました。 対象は shadcn/ui(Radix 1.2.6)、MUI 9.3.1、Chakra UI 3.36.1、Mantine 9.5.1。props のシグネチャは 4 ホストで同一なので、違いはライブラリだけになります。 この層は、育ったプロダクトがそうなるように各ライブラリから乖離させてあります。 ドメイン enum(visibility、tone)を持ちます。size ではなく density。 バラの props ではなく post: PostModel。children ではなく名前付きスロット。onClick ではなく onReplyPress。20 個のシグネチャが全パッケージで一致することはスクリプトが検証します。React 19.2.8 と TypeScript 6.0.3 は 1 つのワークスペースルートに揃えました。
各アームはファイル読み取り・grep・シェルを持つ Claude(Opus 5)のエージェントで、ホストと採点スクリプトを書いたのと同じモデルファミリーです。 どのアームも同じ画面(投稿 4 件、引用、メディア、サイドバーのパネル 2 つを持つタイムライン)を 1 つの .stories.tsx として書きます。 仕様はコンポーネント名とデータを固定してあります。 どのアームも node_modules を持たないため、コンパイラを回しながら直すことはできません。 最初に提出されたファイルを tsc --noEmit で採点し、スクリプトが仕様と突き合わせ、レンダリングは Storybook で抜き取り確認します。
5 条件
各ホスト 20 コンポーネント時の、提出 Story の型エラー数(「クラッシュ」はレンダリング時の例外)。 最初の条件以外は大きい規模でも実行しました(宣言と Registry は 240、ソース条件は 80 / 240)。 該当する全実行がゼロだったため、表は何かが起きる唯一の規模を示しています。
| ホスト | 仕様のみ | + 型宣言 | + Registry | + ソース | ソース + Registry |
|---|---|---|---|---|---|
| shadcn/ui | 36、クラッシュ | 0 | 0 | 0 | 0 |
| MUI | 33、クラッシュ | 0 | 0 | 0 | 0 |
| Chakra UI | 26 | 0 | 0 | 0 | 0 |
| Mantine | 24 | 0 | 0 | 0 | 0 |
「+ 型宣言」は別リポジトリ運用の実像です。 デザインシステムがコンパイル済みパッケージとして届き、エージェントは node_modules を読むのと同じ形で .d.ts を読みます。 「+ Registry」は分離実験です。Registry の出力だけを渡す条件は実務では稀ですが、どの情報が効いていたのかを特定できるのはこの腕です。
欠けていたのは API であり、それを運ぶものなら何でも足ります。 仕様のみの画面は、内容はほぼ正しいです。 引用、メディア、公開範囲、固定されたカウントは仕様と一致しています。 間違っているのは API であり、それも派手に、4 ライブラリを通じて同じ形になります。 名前付きスロットを取るカードへの children。post モデル 1 つを取るコンポーネントへ渡されたバラの replyCount / liked。onReplyPress に対する onReply。label に対する timestamp。 意味は仕様が供給していました。API は誰も供給していませんでした。 それをソース、型宣言、Registry のどれで供給しても、同じエージェントが同じクリーンな画面をどの規模でも出します。 大きいホストに仕込んだ紛らわしい名前 (PostCardCompact、SearchBar など)を選んだ例は、ソース無しの全 20 試行を通じてゼロでした。
型由来の運び手はすべて、描画の慣習という死角を共有します。 MUI だけは PostAuthorLine が handle に @ を付けずにそのまま描画するので、正しいデータは MUI では "@rin"、他では "rin" になります。 ソースを読んだ MUI の 6 アームは全員正解しました。 型だけから書いたアームはおおむね外しました。Registry のみは 3 回中 2 回、型宣言のみは 2 回中 2 回。 画面はコンパイルが通ったまま Rin Amano rin と表示されます。 型が言えるのは handle: string までで、コンポーネントがそれをどう扱うかはソース、Story、スクリーンショットにしかありません。 この種の慣習をコンポーネントに埋めているなら、型レベルのインターフェイスはそれを運びません。Registry の実在する限界であり、パッケージが同梱する .d.ts にとっても同じ限界です。
ソースに手が届くなら、Registry は何も変えません。 ソースが見えるセルは全規模でゼロです。 コンパイルが通る誤りも互角で、片側 12 試行あたり紛らわしい名前の誤選択が 1 回ずつ(うち 1 回は両候補のファイルを読み、警告も受けた上での選択)。 似た名前から選ぶのは判断であり、どの参照手段もそれを肩代わりしません。
各運び手の読解コスト
実際に読まれた量ではなくコーパスの大きさです。 ソース全読は上限値(grep を使うエージェントはこれより少なく読む)。props のみは対になる下限値(*Props インターフェイスブロックだけ。どこを読めばよいかを最初から知っている前提)。 型宣言は出力された .d.ts。Registry は component list 全件 + 画面が使う 18 コンポーネント分の component inspect。
| コンポーネント数 | ソース全読 | ソース props のみ | 型宣言 | Registry |
|---|---|---|---|---|
| 20 | 20〜27KB | 5.5〜6KB | 10.9〜17.6KB | 12.5〜16KB |
| 80 | 75〜110KB | 21〜27KB | 38.8〜45.7KB | 22〜25KB |
| 240 | 231〜332KB | 64〜80KB | 114〜136KB | 44〜49KB |
どの曲線もホスト規模に対して線形で、Registry の係数が最小です。20 コンポーネントではどれも安く、Registry が買うものはありません。240 では、パッケージが同梱する型宣言の 3 分の 1、狙い読みの props のみをも下回ります。 集約された一覧が、ファイル単位の探索を置き換えるからです。 この表が支える主張は意図的に控えめで、他のどの運び手とも同じ出力を、デザインシステム規模では最小の読解量で、というものになります。
残る 2 つの仕組み
Screen JSON バリデーション。 対象は静的画面(このルートが想定する種類の画面。今回のタイムラインは関数 props を必要とするため対象外になります)。 静的画面では両ルートとも 4 ホストすべてでクリーンな Story を生成し、JSON ルートは型検査なしで 2 往復のうちに収束しました。 エラーは正確です (ノード指定つきで、正しい選択肢が列挙されます)。 ただし 2 つの但し書きが要ります。 捕まえたのはすべて形式自体の摩擦(schemaVersion の欠落)だったこと。 そしてバリデータ自身の最終出力が、json 型の props は検査されずそのまま Story に書かれると明言していること。 この画面の意味はまさにそこに住んでいます。 仕組みとしてのループは機能しています。 だがこの実験では意味検査層が何かを捕まえる姿は示せませんでした。 上流の component inspect が、それが捕まえるはずの誤りを先に防いでいたからです。
実装への引き継ぎ。 承認済み Story から実際の状態を持つページへ、story import + screen context の有無で比較すると、両側ともエラーゼロ、配線も完全でした。 ただし配線の一覧は仕様自身が列挙していたので、この同点は設計上の同点でもあります。 より重要な数字は別にあります。 このベンチマークの承認済み Story 24 本のうち 21 本は component + args 形式の CSF です。story import はそれを読めません(STORY_NOT_FOUND。ドキュメント化されたフォールバックは Story をテキストとして読むこと)。 読めるのは render 形式(自身の screen generate が出力する形式)で、そちらではツリーを忠実に復元しました(28 ノード、fixtures 7 個)。
この測定が示さないこと
- ホストは合成であり、乖離レシピはダイヤルです。 仕様のみ条件のエラー数は、レシピが適用した改名の数の関数になります。 レシピはハーネスと共に公開します。 あの列は「この程度の乖離が生むもの」であって定数ではありません。
- 各セル 1 回、繰り返しなし。 コンパイルが通る誤りの頻度(12 試行あたり 1〜2 件)は、追加の 1 回でどのセルも動き得る領域にあります。
- 仕様がコンポーネント名とデータを固定しているため、どのコンポーネントを使うかをアームが決める場面はありませんでした。 ワークフローの前半にあたる探索とキュレーション(Registry の Story ベースの推奨が効く領域)は未測定です。
- 型宣言の条件は、このホストから出力した
.d.tsを使いました。Registry が読むのと同じ JSDoc を含んでいます。 宣言に JSDoc を残さずビルドされたパッケージは、ここで測った条件より暗くなります。 - 240 コンポーネントは最大級のデザインシステムより 1 桁小さく、紛らわしい名前のファミリーは 220 個中 20 個にとどまります。
- ホスト、フィラー、全画面、採点スクリプトのすべてを同じモデルファミリーが書きました。 意味の検査は正規表現と的を絞った目視で、全数の人間レビューではありません。
実コードでの抽出品質(props が型から取れた率、バレルインポートの慣習、抽出できないパターン)は別の問いであり、実運用のデザインシステムで別に測定しています: Component Registry。