indexファイル(バレルファイル)不使用のすゝめ
はじめに
どうも。ニートですわ~。
この記事を拝見しました。そして、僕も同様の理由で「バレルファイルいいじゃんこれ」と思って使っていた時期がありましたが、現在では使用しないようにしています。
以下に理由を書いていきます。
バレルファイルって?
バレルファイル(Barrel File)とは、複数のモジュールをまとめて再エクスポートするファイルのことです。主にindex.ts
やindex.js
という名前で作成され、ディレクトリ内の複数のファイルからエクスポートされた関数、クラス、定数などを一箇所にまとめて外部に公開する役割を持ちます。
具体例
例えば、Reactのコンポーネントを管理するディレクトリに以下のようなファイル構成があるとします
components/
├── Button.tsx
├── Input.tsx
├── Modal.tsx
└── index.ts // バレルファイル
各コンポーネントファイルは以下のようになっています
export const Button = () => {
// ボタンの実装
};
export const Input = () => {
// インプットの実装
};
export const Modal = () => {
// モーダルの実装
};
そして、バレルファイル(index.ts
)では以下のように各コンポーネントを再エクスポートします
export { Button } from './Button';
export { Input } from './Input';
export { Modal } from './Modal';
使用方法
バレルファイルを使用することで、外部からは以下のようにまとめてインポートできるようになります
// バレルファイルを使わない場合
import { Button } from './components/Button';
import { Input } from './components/Input';
import { Modal } from './components/Modal';
// バレルファイルを使う場合
import { Button, Input, Modal } from './components'; // ./components/indexとすら書かなくて良い
このように、複数のファイルから個別にインポートする必要がなくなり、一つのインポート文でまとめて取得できるのがバレルファイルの主な利点とされています。また、可読性が向上します。
多くのライブラリでバレルファイルを使用したエクスポートが行われています。
バレルファイルを使わない理由
1. バンドルサイズの増大(Tree Shakingの阻害)
最も大きな問題は、Tree Shakingが効かなくなる可能性があることです。
// components/index.ts(バレルファイル)
export { Button } from './Button';
export { Input } from './Input';
export { Modal } from './Modal';
export { Table } from './Table';
export { Chart } from './Chart'; // 重いライブラリを使用
// 使用する側
import { Button } from './components'; // Buttonしか使わないのに...
上記の場合、Buttonしか使用していないにも関わらず、バンドラーはcomponents/index.ts
を通してすべてのコンポーネントを解析する必要があります。特にChartコンポーネントが重いライブラリ(例:D3.js、Chart.js)を使用している場合、それらも一緒にバンドルに含まれる可能性があります。結果として、ビルド後のファイルサイズが意図せず肥大化します。
特に、Next.jsのようなもともとバンドルサイズが大きいフレームワークでは、バレルファイルを使用するとバンドルサイズがさらに大きくなり、デプロイ先のファイルサイズ制限などに引っかかる等の問題が発生する可能性があります。
2. 循環依存の発生リスク
バレルファイルは循環依存を引き起こしやすくします
// components/Button.tsx
import { Modal } from './index'; // バレルファイルを参照
export const Button = () => {
// Modalを使用
};
// components/index.ts
export { Button } from './Button'; // 循環依存発生!
export { Modal } from './Modal';
この場合、Button.tsx
→ index.ts
→ Button.tsx
という循環参照が発生し、実行時エラーやビルドエラーの原因となります。
このような例のバカ実装は現実的ではないかもしれませんが、プロジェクトが大規模になり、依存関係が複雑化すると、意図せず循環依存を発生させてしまうリスクが高まります。
3. バンドラーのパフォーマンス低下
Viteでの問題
Viteでは、特に大規模なプロジェクトでは、バレルファイルを使用すると、開発モードで多くのファイルが一気にダウンロード・コンパイルされ、HMR(Hot Module Replacement)が非常に遅くなるという問題が報告されていたりします。
Webpackでの問題
Webpack環境でも、バレルファイル経由で動的importを行うと、バレルでexportされている全てのモジュールがバンドル対象となり、chunk分割やtree shakingが効きにくくなることが報告されています。
// 動的インポートの効果が薄れる例
const Component = await import('./components'); // バレル内の全モジュールがバンドル対象になる可能性がある
const { Button } = Component;
// より効率的
const { Button } = await import('./components/Button'); // Buttonのみがバンドル対象
sideEffectsフラグの限界
package.json
でsideEffects: false
を設定することで、バレルファイル経由でもtree shakingが効くケースがありますが、副作用のあるファイル(CSSインポートなど)が混ざると正しく動作しません。
4. 名前空間インポートの誤解を招く記法
名前空間インポートを使用した場合、クラスのメソッドのように見える可能性があります
import * as Components from './components';
// これはクラスのstaticメソッドのように見える
<Components.Button />
<Components.Modal />
// 実際は個別のコンポーネント
上記のようなReactコンポーネントならまだしも、React以外のコンテキストでは、より混乱を招きかねません。
import * as Utils from './utils';
// これがstaticメソッドなのか、関数なのか分かりにくい
Utils.formatDate();
Utils.validateEmail();
まあこれは僕のコンテキストウィンドウサイズの問題かもしれませんが
5. TypeScriptの型解決パフォーマンスの低下
TypeScriptコンパイラは、バレルファイルを通すことで余計な型解決を行う必要があります
// 直接インポート
import { Button } from './Button'; // Button.tsxの型のみ解決
// バレルファイル経由
import { Button } from './index'; // index.tsが参照するすべてのファイルの型を解決しようとする可能性がある
大規模なプロジェクトでは、この差が顕著にコンパイル時間に影響を与え、開発体験を損なうことがあります。
6. IDE/エディタでの「定義に移動」の問題
バレルファイルを使用すると、IDEの「定義に移動(Go to Definition)」機能でバレルファイルに飛んでしまい、実際の実装にたどり着くまでに余計なステップが必要になります。
この点は開発者の好みにもよりますが、僕は実装箇所に直接ジャンプできないことにストレスを感じます。
7. リファクタリングの複雑化
ファイルの移動や名前変更時に、バレルファイルも合わせて更新する必要があり、リファクタリングが複雑になります。
VSCodeなどなら、ファイル移動の場合GUIで行うと参照先も自動的に更新してくれることが多いのでいいですが、エクポートする関数名を変えた場合などは手作業で行う必要があるので、手間が増えます。
(多くの場合名前を変更したらエラーが出るので修正漏れは少ないと思いますが)
// Button.tsx を PrimaryButton.tsx にリネーム
// → index.ts も更新が必要
export { PrimaryButton as Button } from './PrimaryButton'; // エイリアスが必要?
export { PrimaryButton } from './PrimaryButton'; // それとも名前も変更?
8. 部分的インポートの阻害
ESModulesの部分的インポートの恩恵を受けられません
// lodashの例
import { debounce } from 'lodash/debounce'; // debounceのみ
import { debounce } from 'lodash'; // lodash全体(バレルファイル的)
// 自作モジュールでも同様
import { formatDate } from './utils/formatDate'; // formatDateのみ
import { formatDate } from './utils'; // utils全体
9. Monorepo環境での問題
Monorepo環境では、パッケージ間の依存関係が複雑になりがちです。バレルファイルがあると、パッケージ間で意図しない依存関係が生まれ、ビルドや依存関係の解決が困難になる可能性があります。
これらは一例で他にもあるかもですが、要するにバレルファイルを使用すると、不要なモジュールまで読み込まれてしまい、それが様々な問題を引き起こす可能性があるということです。
ならどうすれば?
結論として、アプリケーション開発においてはバレルファイルを使わず、モジュールを直接インポートするのが最も安全で確実な方法です。
// 結局これでいい
import { Button } from './components/Button';
import { Input } from './components/Input';
確かにReactコンポーネントファイルの先頭に大量にインポート文が並ぶと、一見すると読みにくいかもしれません。しかし、VSCodeなどのモダンなエディタには強力な自動インポート機能があり、「このインポート元はどこだっけ?」となってもCtrl + click
(Cmd + click
)ですぐに定義元へ移動できます。
また、プロジェクト全体で一貫性のある命名規則を採用し、コンポーネントや関数の役割が名前から推測できるように心がけることで、可読性の問題は十分にカバーできるというのが僕の意見です。
緩和策はあるけれど…
いくつかのバンドラーやフレームワークは、バレルファイルの問題を緩和する機能を提供しています。
-
"sideEffects": false
:package.json
にこのフラグを設定すると、バンドラーに対して「このパッケージ内のファイルは副作用がない」と伝え、より積極的にTree Shakingを行うよう促せます。しかし、CSSのインポートなど、実際には副作用を持つファイルが含まれていると正しく機能しません。 -
Next.jsの
optimizePackageImports
: Next.js 13.5以降では、指定したライブラリ(バレルファイルでエクスポートしているもの)をビルド時に自動で直接インポートに変換してくれる実験的機能があります。
これらの機能は有効な場合もありますが、万能ではなく、設定が複雑であったり、意図通りに機能しなかったりするケースも報告されています。そのため、やはり基本は直接インポートを徹底するのがシンプルで間違いのないプラクティスだと考えています。
まあ、バレルファイルを使ったインポートの見た目はすごいスタイリッシュに見えますけどねw
おわりに
決してバレルファイルを「絶対に使うな」という趣旨の記事ではありません。例えば、ライブラリの作者が公開APIを定義するといった特定の文脈では、今でも有効なパターンです。
しかし、アプリケーション開発、特にフロントエンドの文脈においては、上記で挙げたようなパフォーマンスや保守性の観点から多くのデメリットが存在します。最近のバンドラーのTree Shakingは高性能ですが、それでもバレルファイルが原因で最適化が妨げられる可能性があることは、知っておいて損はないでしょう。
特に大規模なコードベースになるほど、その影響は顕著になります。一方で、個人開発や小規模なプロジェクトであれば、問題になることは少ないかもしれません。
バレルファイルはインポートをすっきりと見せる便利な機能ですが、その裏にあるトレードオフを理解した上で、採用するかどうかを判断するのが良いと思い、この記事を書きました。
ニート脱却したいな~ では。
Discussion