背景
ReactやTypeScriptで書かれたUIコンポーネント群を index.ts でまとめる、いわゆるBarrelファイル構成は広く使われている。利用側はフォルダ単位でimportでき、公開APIの境界もコードを読むだけで把握できる。
ただしこの構成は、コンポーネントが数十から数百規模に達すると、Tree Shakingの効きにくさ・テストやdev serverの初期化連鎖・AIコーディングエージェントによるコード追跡コストとして表面化する。Barrelの利点である「入口の自明性」を保ったまま、これらのコストを抑える設計を組めるか。
Barrelファイルとは何か
Barrelファイルとは、index.ts などで同一ディレクトリ内のexportをまとめて再exportするファイルである。
export { ArticleCard } from './ArticleCard';
export { ArticleCardHeader } from './ArticleCardHeader';
利用者は @/components/UI/ArticleCard のようにフォルダ単位でimportする。
Barrelファイルの利点
Barrelファイルが広く使われてきた理由を挙げる。
- import入口が自明である。利用者はフォルダ名でimportでき、内部のファイル名を意識しなくてよい。
- 公開APIの設計がLinterなしで伝わる。
index.ts に書かれたexportが公開API、書かれていないものは内部実装、という規約がコードを読むだけで把握できる。規約ドキュメントやlintルールがなくても新規参加者に直感的である。 - 内部構成の変更を隠蔽できる。内部のファイル分割やリネームを
index.ts で吸収すれば、利用側のimportパスは変わらない。 - import文の形式が揃う。利用側の
import { Foo } from '@/components/UI/Foo' が一定のフォーマットになり、人間にとって読みやすい。
これらは小規模から中規模のコードベースで十分に機能する。次節で扱う問題は、規模が一定を超えてから顕在化する。
Barrelをやめる主目的はパフォーマンスとAIのためにある
Barrelをやめる主目的は、単に index.ts を削除することではない。主目的は3点である。
- Tree Shakingの成立条件を増やさない
- モジュール初期化の連鎖を断ち切る
- AIコーディングエージェントの追跡コストを下げる
Tree Shakingの成立条件が増える
Barrelは複数モジュールの再exportを集約するため、バンドラが追跡するモジュールグラフを拡大させやすい。webpack の公式ドキュメントによれば、Tree Shakingの成立はESM構文・sideEffects 指定・CommonJS変換の有無・production最適化の組み合わせに依存する。Barrel経由ではこれらの成立条件が増えるため、利用していないexportがバンドルに残る場面が出やすい。
モジュール初期化が連鎖する
import { ArticleCard } from '.../ArticleCard' を経由してBarrelに到達すると、Barrelで再exportされる全モジュールがロード対象になる。テスト時の初期化コストが膨らみ、dev serverの再評価範囲も広がる。Biomeの noBarrelFile ルールが lint/performance カテゴリに置かれている根拠もここにある。
AIコーディングエージェントの追跡性が落ちる
ランタイムだけでなく、コード解析の文脈でも同じ問題が出る。コーディングエージェントがシンボル定義をたどるとき、Barrel経由だと「Barrelファイルを開く→再export元を読み込む」の二段を踏む。コンテキストウィンドウに不要な再export文が乗り、トークン消費と読み取りステップが増える。直接importであれば、エージェントは公開ファイル1つを読めば済む。
主目的を満たすための設計
これらの主目的を満たすには、次の設計をセットで導入する必要がある。
- Barrelファイルを禁止する
- 公開入口は同名ファイルにする
- 内部実装は同名ディレクトリに置く
- exportはnamed exportに寄せる
- BiomeでBarrel・default export・private importを制限する
- 厳密な依存境界はdependency-cruiserなどの専用ツールで補う
推奨ディレクトリ構成
公開入口を同名ファイルとし、内部実装を同名ディレクトリに置く。
src/components/UI/
ArticleCard.tsx # 公開入口
ArticleCard/
Header.tsx # 内部実装
Excerpt.tsx
Tags.tsx
利用側はフォルダではなく公開ファイルを直接importする。
import { ArticleCard } from '@/components/UI/ArticleCard';
@/components/UI/ArticleCard は ArticleCard.tsx を解決する。ArticleCard/ 配下は内部実装であり、利用側からは触れない。
Barrel構成との比較
Barrel構成と廃止後の構成を比較する。
Barrel廃止後はTree Shaking・モジュール初期化・AI解析の3点で優位になるが、利用者の迷いやルール強制の弱さを規約とlintで補う必要がある。
実装例: Toastコンポーネント
公開入口は同名ファイル Toast.tsx、内部実装は Toast/ 配下に置く。
src/components/UI/
Toast.tsx
Toast/
ToastView.tsx
useToastState.ts
公開入口の Toast.tsx は、内部実装を組み立て、公開コンポーネントを返す。
// src/components/UI/Toast.tsx
import { ToastView } from './Toast/ToastView';
import { useToastState } from './Toast/useToastState';
export function Toast() {
const state = useToastState();
return <ToastView state={state} />;
}
利用側はnamed exportでimportする。
import { Toast } from '@/components/UI/Toast';
避けるべきimportは、内部実装への直接importである。
// 避ける: 公開APIを経由していない
import { ToastView } from '@/components/UI/Toast/ToastView';
このimportは公開APIを経由していないため、後から内部構成を変えにくくなる。ToastView を分割した瞬間、利用側のimportパスが破綻する。Barrelが暗黙に提供していた「内部構成の隠蔽」は、Barrel廃止後はBiomeの内部import制限で部分的に代替する。完全な隠蔽には別途dependency-cruiserなどで補強する必要がある(後述)。
環境
- Biome 2.4.14
- Next.js 16.2.4
なお、noRestrictedImports.options.patterns はBiome v2.2.0以降、overrides[].includes はBiome v2.xの記法であるため、Biome 1.xでは動作しない。
Biomeで制約を強制する
設計を口約束で運用するとすぐ崩れる。lintで強制する必要がある。
ESLintでも同等の制約は組めるが、次のように複数プラグインの組み合わせが必要になる。
設定が分散するため、本記事ではこれらを単一のlinterで完結させられるBiomeをベースに説明する。
Biomeで以下のルールを有効化し、Barrel・default export・private importを構造的に制限する。
{
"linter": {
"rules": {
"performance": {
"noBarrelFile": "error",
"noNamespaceImport": "warn",
},
"style": {
"noDefaultExport": "error",
"useImportType": "warn",
"useExportType": "warn",
"noRestrictedImports": {
"level": "error",
"options": {
"patterns": [
{
"group": ["@/**/index", "@/**/index.*"],
"message": "index/barrel importは禁止です。公開ファイルを直接importしてください",
},
{
"group": ["@/components/UI/*/*", "@/components/UI/*/**", "!@/components/UI/Layout/*"],
"message": "UI moduleの内部ファイルを直接importしないでください",
},
],
},
},
},
},
},
}
各ルールの役割を整理する。
noBarrelFile: 再exportのみのファイルを禁止するnoDefaultExport: default exportを禁止し、named exportに寄せるnoRestrictedImports: index.* への依存と、UI/*/* のような内部ファイルへの直接importを禁止するuseImportType / useExportType: 型のimport・exportを import type / export type に統一する
Next.jsの page.tsx や layout.tsx はdefault exportが前提のため、ファイル単位で例外を設定する。
{
"overrides": [
{
"includes": ["src/app/**/page.tsx", "src/app/**/layout.tsx"],
"linter": {
"rules": {
"style": {
"noDefaultExport": "off",
},
},
},
},
],
}
なお、noRestrictedImports のpatternsは @/... のようなエイリアス経由のimportに適用される。同一モジュール内からの相対パスでの内部import (例: ./Toast/ToastView 形式) は対象外であるため、Biomeだけでは相対パス経由の参照までは塞げない。相対importの境界制御や循環依存の検出が必要であれば、dependency-cruiserなどの専用ツールで補う。
まとめ
Barrelファイルはimport入口を自明にする点で有用だが、規模が大きくなるとTree Shakingの成立条件を増やし、モジュール初期化を連鎖させ、AIコーディングエージェントの追跡コストを増やす。Barrelを廃止するなら、index.ts を削除するだけでは足りない。同名公開ファイル、named export、private import制限をセットで導入する必要がある。これによりコロケーションを維持しながら、これらのコストを抑えられる。
参考