見出し画像

バレルファイルの真実:コード管理の利便性とパフォーマンスへの影響を理解する

バレルファイルとは?

バレルファイルとは、他のファイルからエクスポートされた複数のモジュールを再エクスポートする単一のファイルです。なぜこのような仕組みが必要なのでしょうか?それは、モジュールにアクセスするための一元化された場所を提供することで、インポートとコードの可読性を向上させるためです。
バレルファイルとその使用方法

/*ui package index.ts*/
export { Button } from './components/Button';
export { Card } from './components/Card';
export { Modal } from './components/Modal';
import { Button, Card, Modal } from "ui"

上記の例を見ると、バレルファイルが関連モジュールをエクスポートする単一のファイルを提供することで、インポートと保守性を簡素化していることがわかります。このため、多くのJavaScriptコンポーネントライブラリがパッケージにこの方法を採用しています。
この方法に従うことで、モジュールとパッケージの内部実装を隠蔽することができます。コードがクリーンで構造化しやすく、保守も容易であり、これは私たちが望むところですが、ここで問題が生じます。バレルファイルの使用にはメリットがありますが、ビルド時間、バンドル、アプリケーションのパフォーマンスに関連する重要な問題を引き起こす隠れたコストが存在します。
この記事では、バレルファイルがプロジェクトにどのような影響を与えるかを検討し、NextJsプロジェクトでバレルインポートの有無による変化を示す例も含めて説明します。

バレルファイルのコスト

ビルド時間とビルドサイズの増加

バレルファイルを使用する際、副作用が適切に設定されていない場合、Webpackなどのツールはツリーシェイキングが不明確になるため、必要以上のファイルを解析して含めてしまう可能性があります。これにより、バンドラーが未使用のエクスポートを効率的に特定して削除することが困難になります。すべてをエクスポートする単一のファイルを使用すると、単一のコンポーネントのみをインポートする場合でも、バンドラーは正しいエクスポートを特定するためにバレルファイル全体を解析する必要があります。ツリーシェイキングが失敗し、未使用のモジュールが最終ビルドに含まれると、ビルドサイズが大きくなってしまいます。
NextJs 14shadcnコンポーネントを使用して作成したデモプロジェクトで、バレルファイルによってビルドサイズと時間がどのように増加するかを見てみましょう。デフォルトではshadcnはバレルエクスポートを提供せず、ドキュメントでは直接インポートを使用しています。このデモでは、すべてのコンポーネントを再エクスポートするバレルファイルを作成しました。
UIからバレルエクスポートされたButtonコンポーネントをインポートする場合について説明します。これから、バレルエクスポートによって引き起こされる問題とその解決策について、結果を参考に見ていきましょう。

UIフォルダからすべてのコンポーネントがエクスポートされるバレルエクス
UIフォルダのindexファイルからのコンポーネントのインポート
開発環境でのページのコンパイルに要するモジュールと時間
ビルド時のバンドルサイズ
バレルインポート使用時のビルドバンドルの分析

上記の結果から分かるように、バレルファイルからButtonコンポーネントをインポートしただけで、ページの初期ロードJSが255KBにもなっています。これは単なるボタンコンポーネントとしては大きすぎるサイズです。

パフォーマンスへの影響

前述の通り、バレルファイルの使用によってビルド時間とサイズが予想以上に大きくなり、アプリケーションのパフォーマンスに影響を与えます。バンドルサイズが大きくなると、ブラウザがダウンロード、解析、実行すべきコードが増え、ページの読み込みが遅くなり、パフォーマンスとユーザー体験が低下します。

循環依存関係

大規模なプロジェクトでは、バレルファイルの使用により、相互に依存するファイル間で循環依存関係が発生する可能性があります。ES ModulesやCommonJSなどのほとんどのモジュールシステムは循環インポートを許可していますが、予期せぬ動作やランタイムエラー、デバッグが困難な問題を引き起こすことが多く、すべての依存関係を解決するのに時間がかかるため、ビルド時間も遅くなる可能性があります。
私たちのデモ環境で循環インポートを使用したところ、最大コールスタックの問題が発生してビルドが失敗しました。そのため、開発中に循環依存関係の問題を検出するための適切なESLintの設定が必要です。
状況をよりよく理解するために、以下に循環依存関係の簡単な例を示します。

// components/index.js (バレルファイル)
export { Button } from './Button';
export { Card } from './Card';
export { Modal } from './Modal'
// components/Button.js
import { Card } from './index';
const Button = () => { /* ... */ };
export Button;
// components/Card.js
import { Button } from './index';
const Card = () => { /* ... */ };
export Card;

バレルファイルのコストを削減する

バレルファイルのコストについて理解したところで、これらを効果的に管理し最小化する方法を探っていきましょう。

ツリーシェイキングの最適化

未使用のモジュールがバンドルに含まれないようにするには、プロジェクトでツリーシェイキングを最適化する必要があります。package.jsonの"sideEffects"プロパティを使用して、プロジェクト内のどのファイルがpure(純粋)であり、未使用の場合にビルド時に安全に除去できるかを指定できます。副作用のあるファイルとは、グローバル変数の変更やDOM操作など、そのエクスポートが使用されていなくても、スコープ外に影響を与える動作を行うファイルを指します。
バレルからエクスポートされるすべてのモジュールがpureな場合、"sideEffects":falseを使用して、未使用のモジュールを除去できることを指定できます。

{
  "name": "our-project",
  "sideEffects": false
}

ただし、バレル内のモジュールに副作用がある場合は、プロジェクトに必要な副作用がビルド時に除去されないよう、副作用のあるファイルを指定する必要があります。

{
  "name": "our-project",
  "sideEffects": ["./src/side-effect-file.ts"]
}

デモ環境でのテストでは、sideEffectsをfalseに設定しても未使用のコンポーネントのツリーシェイキングは行われませんでした。しかし、Next.jsの実験的機能であるoptimizePackageImportsを使用することで、ツリーシェイキングを実現することができました。
optimizePackageImportsを使用することで、UIフォルダからエクスポートされた未使用のコンポーネントをツリーシェイキングし、バンドルサイズを削減することができました。Next.jsチームは、バレルファイルの最適化についての詳細をこちらの記事で説明しています。

バレルファイルからのButtonコンポーネントのインポート
パッケージインポートを最適化するためのNext設定ファイル
未使用コンポーネントのツリーシェイキング後のバンドル

上記の結果から、ツリーシェイキングが成功し、初期ロードJSが255kBから92.4kBに削減されたことが分かります。

直接インポートの使用

NextJsでsideEffectsやoptimizePackageImportsを使用してもツリーシェイキングが達成できない場合、バレルファイルからのインポートではなく、直接インポートを選択することができます。直接インポートを使用することで、バンドルに不要なコードが含まれるリスクを軽減できます。
以下のような方法ではなく:

バレルファイルからButtonコンポーネントをインポートする例

次のように、コンポーネントを直接そのファイルからインポートします:

コンポーネントの直接インポート

結果を確認してみましょう

直接インポート後のコンパイル時間とモジュール
コンポーネントを直接インポートした後のバンドル
バンドル分析

直接インポートを使用しても、未使用のコンポーネントのツリーシェイキングで同様の結果が得られることがわかります。総バンドルサイズが約 48% 削減されたことで、サイトのパフォーマンス向上が期待できます。最適化後にコンパイルされるモジュールも減少し、サイトのパフォーマンスが向上しています。このデモプロジェクトは小規模なため、ビルド時間の差異は最適化のないバレルファイルを使用した場合とほぼ同じですが、大規模なプロジェクトではモジュール数が多いため、かなりの時間差が生じます。

追加の注意点

上記の解決策はバンドルサイズの削減に完璧に機能しますが、テスト中に依存関係に関する他の問題も発見されました。
他のコンポーネントに依存するコンポーネントをインポートする場合、バレルインポートの使用は避けるべきです。すべての上記の最適化を適用したテスト中、ButtonコンポーネントにCalendarコンポーネントをバレルエクスポートファイルからインポートしました。

バレルファイルからCalendarコンポーネントをButtonコンポーネントにインポートする

この更新後、ページでButtonコンポーネントを使用すると、直接インポートとoptimizePackageImportの両方がバンドルサイズの削減に失敗しました。これは、ButtonコンポーネントがCalendarコンポーネントに直接依存し、バレルインポートによって付随する他のすべてのコンポーネントに間接的に依存することになり、バンドルサイズが増加したためです。そのため、意図しない依存関係を排除するために、バレルを使用するのではなく、特定のコンポーネントを直接インポートすることをお勧めします。

結論

結論として、バレルファイルはインポートを簡素化し、コードの整理を改善しますが、プロジェクトにもたらす隠れたコストはアプリケーションのパフォーマンスに大きな影響を与える可能性があります。しかし、sideEffectsでツリーシェイキングを適切に設定したり、NextJsのoptimizePackageImportsを使用したり、バレルからのインポートを避けてコンポーネントを直接インポートしたりすることで、これらの問題を解決でき、バンドルサイズを削減してパフォーマンスを向上させることができます。
したがって、次のアプリケーションやパッケージを構築する際は、バレルファイルの実装を計画している場合、適切な最適化戦略を実装してください。

デモリポジトリ

https://github.com/nareshban1/barrel-demo

参考文献

バレルファイルの使用を停止してくださいなぜ
Next.jsでパッケージインポートを最適化した方法 
バレルファイルと今すぐ使用を中止すべき理由小さな決定もアプリケーションに大きな影響を与えることがあります。


この記事は、2024 年 12 月に弊社のエンジニア Naresh Ban が執筆し、日本語に翻訳したものです。
英語版はこちらをクリックしてください。
https://articles.wesionary.team/the-hidden-costs-of-barrel-files-25de560b9f63


採用情報

私たちはプロダクト共創の仕組み化に取り組んでいます。プロダクト共創をリードするプロダクト・マネージャー、そして、私たちのビジョンを市場に届ける営業メンバーを募集しています!


開発パートナーをお探しの企業様へ

弊社は、グローバル開発のメリットを活かし、高い費用対効果と品質を両立しています。経験豊富で多様性のあるチームが、課題を正しく理解し、最適なシステムと優れた体験を実現します。業務システムの開発、新規事業の開発、業務効率化やDX化に関するお困りごと、ぜひ弊社にご相談ください。