コンポーネントベースのパッケージで tree shaking を効かせるには?設計・配布・ビルド最適化のポイントを解説
フロントエンド開発でコンポーネントベースのパッケージを配布するとき、多くのチームが気にするのは「使いやすい API をどう作るか」「見た目や振る舞いをどう統一するか」といった点です。しかし、実務で長く使われる UI ライブラリや design system では、それと同じくらい重要なのが bundle size です。どれだけ使いやすいコンポーネント群を用意していても、アプリケーション側で必要な部品を数個 import しただけなのに、ライブラリ全体の大部分が bundle に残ってしまうようでは、配布物としての完成度は高いとは言えません。とくに、component-based packages は小さな部品の集合として使われることが多いため、「必要なものだけを取り込めるかどうか」が利用体験に大きく影響します。
ここで重要になるのが tree shaking です。tree shaking は未使用コードをビルド時に落とすための仕組みとしてよく知られていますが、実際には「ESM で出せば自然に効く」というほど単純ではありません。export 設計が雑だったり、side effects の扱いが曖昧だったり、コンポーネントと CSS や helper が密結合していたりすると、理論上は削れるはずのコードが bundle に残りやすくなります。つまり、tree shaking はビルドツールだけの問題ではなく、パッケージの設計、配布形式、依存関係の切り方まで含めた総合的なテーマです。本記事では、component-based packages において tree shaking を正しく機能させるために、どのような考え方と設計が必要なのかを、実務寄りの観点から順番に整理していきます。
1. tree shaking とは?
tree shaking とは、アプリケーションから参照されていない export をビルド時に解析し、最終 bundle から落とす最適化のことです。名前のとおり、木の枝を揺らして不要な葉を落とすようなイメージで使われますが、本質は「静的に解析可能な import / export 構造があること」と「未使用部分を安全に除去できること」にあります。つまり、単にファイルを分けているだけでは足りず、ビルドツールが依存関係を追いやすい形でパッケージが設計されている必要があります。ここを誤解すると、「ちゃんと分割したのに bundle が小さくならない」ということが起こりやすくなります。
コンポーネントパッケージで tree shaking が重要なのは、利用側がライブラリ全体ではなく、一部の部品だけを使うことがほとんどだからです。たとえば、Button、Modal、Tabs、Table、Icon などをまとめた UI パッケージがあったとして、あるアプリケーションが必要とするのはそのうち数個だけかもしれません。それにもかかわらず、構造や配布形式の問題で使っていないコンポーネントまで bundle に残ると、読み込み性能や初期表示速度、さらにキャッシュ効率にも影響します。つまり、component-based packages における tree shaking は、単なる build optimization の話ではなく、「利用側が本当に必要なものだけを受け取れるようにする配布設計」の話でもあります。
1.1 未使用コード削除の基本的な考え方
未使用コード削除の基本は、ビルドツールが import / export の関係を静的に理解できることにあります。ESM はこの点で非常に相性がよく、どの export がどこで使われているかを比較的追いやすいため、tree shaking の前提になりやすいです。一方で、実行時に import 対象が決まるような動的構造や、モジュールを読み込んだ時点で副作用が走るコードが多いと、ビルドツールは「これは本当に消して安全か」を判断しづらくなります。結果として、使っていないように見えるコードも bundle に残りやすくなります。
ここで大事なのは、未使用コード削除は「ビルドツールが賢ければ何とかしてくれる」ものではないということです。ビルドツールはあくまで、パッケージ側が静的解析しやすい形で作られていることを前提に最適化します。つまり、tree shaking を効かせたいなら、配布物そのものが「消されやすい構造」になっている必要があります。これは、書き方の小さな違いではなく、export の切り方、ファイル単位の責務、副作用の有無といった設計の問題です。
1.2 UI ライブラリや design system で影響が大きい理由
UI ライブラリや design system は、チーム全体で共通利用されることが多く、複数のアプリケーションや画面から import されます。そのため、ひとつひとつのコンポーネントが少し重いだけでも、積み重なると大きな bundle size 増加につながります。さらに、ライブラリ利用側は通常、そのライブラリの内部構造までは把握していません。つまり、tree shaking が効くかどうかは利用側の頑張りではなく、配布側の責任に近い部分があります。
また、design system のようなパッケージは「部品数が多い」こと自体が特徴でもあります。だからこそ、「全部を一気に読み込ませないこと」に価値があります。Button だけ使いたいアプリに Table や DatePicker や巨大な icon set まで付いてくるような構造では、共通化のメリットより配布コストのデメリットが目立ってしまいます。つまり、component-based packages における tree shaking は、単なる効率化ではなく、共通基盤を本当に実用的なものにするための条件でもあります。
2. tree shaking が効かない原因はどこにあるのか
tree shaking が効かないとき、多くの人はまず bundler の設定を疑います。もちろん build 設定が原因になることもありますが、実際にはそれ以前のパッケージ設計に問題があることも非常に多いです。たとえば、ESM ではなく CommonJS でしか出していない、side effects の情報が package.json に適切に書かれていない、barrel export が重くなりすぎている、あるいは import した時点でグローバル初期化が走る、といった構造があると、ビルドツールは未使用コードを安全に落としにくくなります。つまり、tree shaking が効かない原因は「最後のビルド」ではなく、「最初の設計」に潜んでいることが少なくありません。
さらに厄介なのは、これらの原因がひとつだけで起きるとは限らないことです。配布形式は ESM でも、内部で副作用を持つファイルを barrel 経由でまとめて export していれば、未使用コンポーネントまで巻き込まれることがあります。あるいは sideEffects を false にしていても、実際には CSS import や自動登録処理が含まれていれば、今度は別の問題が起こりえます。つまり、tree shaking が効くかどうかは、単一の設定ではなく、配布物全体の整合性で決まります。
2.1 ESM で配布していない
もっとも基本的で、なおかつ大きな原因のひとつが、パッケージが ESM で適切に配布されていないことです。CommonJS は実行時の柔軟性が高い一方で、静的解析には不利な場面が多く、tree shaking の観点では扱いづらいことがあります。最近の bundler は CommonJS に対してもある程度最適化できますが、ESM に比べると限界があります。とくに component-based packages のように「一部だけ使う」前提のライブラリでは、ESM-first の設計がかなり重要になります。
そのため、tree shaking を効かせたいなら、少なくとも利用側が ESM entry を確実に選べる構成にしておく必要があります。module フィールドや exports フィールドの設計が曖昧だと、意図せず CommonJS 側が使われることもあります。つまり、「ESM ビルドも作っている」だけでは十分ではなく、「利用側が自然に ESM を解決できるようにする」ところまで含めて配布設計と考えるべきです。
2.2 side effects が曖昧になっている
tree shaking では、「このファイルは import されたとき、export 以外の副作用を持つかどうか」が非常に重要です。もし副作用があるなら、たとえ export が未使用でも、その import 自体を消すと動作が変わる可能性があるため、bundler は慎重になります。逆に、純粋な export だけのモジュールであれば、未使用部分をかなり積極的に落としやすくなります。つまり、side effects の有無は tree shaking の前提条件そのものです。
しかし、実務ではここが曖昧になりやすいです。たとえば、コンポーネントファイルの先頭で CSS を import していたり、グローバルな registry へ自動登録していたり、ログや初期化処理を実行していたりすると、そのファイルは「ただの部品定義」ではなくなります。にもかかわらず、package.json の sideEffects を雑に false にしてしまうと、今度は必要なコードまで落ちる危険があります。つまり、side effects は「あるかないか」を正確に整理する必要があり、あいまいなままでは tree shaking も安定しません。
2.3 re-export の設計が bundle を膨らませることがある
barrel export や re-export は API をきれいに見せるうえで便利です。利用側にとっては import 文が短くなり、エントリポイントも統一しやすくなります。しかし、内部構造によっては、この re-export が未使用コードを巻き込みやすいことがあります。とくに index.ts が大量のコンポーネント、helper、theme、icons、global styles をまとめて参照しているような構造では、「一つの部品を取るために全体を一度触る」形になりやすいです。
もちろん、re-export 自体が悪いわけではありません。問題なのは、何を同じ入口にまとめているかです。副作用のあるファイルや重い依存を barrel へ混ぜると、使っていない export まで bundle 側が慎重に残しやすくなります。つまり、API の見た目を整えるための barrel export であっても、内部では「何をひとつの束として扱うか」をよく考えないと、tree shaking の効き方を弱めることがあります。
3. component-based packages における export 設計の基本
component-based packages で tree shaking を効かせたいなら、export 設計は最初に見直すべきポイントのひとつです。利用側の import が短く分かりやすいことは大切ですが、それだけを優先すると、内部的には大きな束として扱われやすい構造になってしまうことがあります。とくに UI ライブラリでは、コンポーネント、hooks、icons、styles、utils が一緒に export されることも多く、何をどの入口から出すかで build 結果がかなり変わります。つまり、export 設計は API デザインであると同時に、bundle efficiency の設計でもあります。
また、export 設計は利用側の習慣にも影響します。深い内部パスを毎回書かせるのは使いにくいですが、トップレベル入口だけに何でも載せると tree shaking が弱くなりやすいこともあります。そのため、実務では「使いやすさ」と「削りやすさ」のバランスを取ることが重要です。たとえば、トップレベルの named export を基本にしつつ、必要に応じて subpath exports を用意する、といった設計はかなり現実的です。
3.1 barrel export の利点と落とし穴
barrel export の利点は明快です。利用側から見ると import の入口がそろい、@org/ui から必要な部品をまとめて書けるため、見た目がきれいで、ドキュメントも作りやすくなります。とくに design system のように部品数が多いパッケージでは、barrel があるだけで利用体験はかなりよくなります。
ただし、内部の barrel が巨大化すると、コンポーネントだけでなく、theme、global CSS、icons、shared helper まで一緒に参照するような構造になりやすいです。そうなると、使っていない export であっても、bundler が慎重に扱うべき対象が増え、結果として tree shaking の効き方が弱くなることがあります。つまり、barrel export は API を美しくする一方で、中に何をまとめるかを誤ると配布物を重くする可能性があります。
3.2 named export を中心に考える理由
tree shaking の観点では、named export は比較的扱いやすいです。どの export が参照されているかを bundler が判断しやすいため、未使用部分を落としやすくなります。利用側にとっても、どの名前を使っているのかが明示的になるため、API の見通しがよくなります。
一方で、default export はそれ自体が悪いわけではありませんが、named export に比べるとパッケージ全体の設計ルールがぶれやすくなります。とくに barrel や re-export が絡むと、「どれが default で、どれが named なのか」が複雑になりやすいです。つまり、component-based packages を長期運用するなら、tree shaking のためだけでなく、一貫性のためにも named export 中心で考えるほうが安定しやすいです。
3.3 default export が不利になるケース
default export はコンポーネント単体のファイルでは書きやすく感じることがあります。しかし、パッケージ全体で多数の default export を barrel から再公開する構造になると、API 設計が分かりにくくなったり、re-export 周りが複雑になったりしやすいです。また、ツールチェーンによっては named export 前提の最適化設計と相性がよい場合もあり、default 中心にすると一貫性が落ちることがあります。
そのため、default export を全面的に禁止する必要はありませんが、component-based packages では「ライブラリ全体としてどう公開するか」を優先して考えたほうがよいです。単体ファイルの書きやすさより、利用側の分かりやすさと build 時の扱いやすさを重視すると、named export ベースの構造へ寄りやすくなります。
// good: named export
export { Button } from "./button";
export { Modal } from "./modal";
export { Tabs } from "./tabs";
// avoid making barrel import heavy modules unnecessarily
export { createTheme } from "./theme/create-theme";
4. ファイル構成は tree shaking にどう影響するのか
ファイル構成は、一見すると単なる整理整頓の話に見えるかもしれません。しかし、tree shaking の効き方にかなり影響します。なぜなら、どの責務をどのファイルへ閉じ込めているかによって、bundler が依存関係を切り分けやすいかどうかが変わるからです。ひとつのファイルが複数の部品、helper、style、初期化処理を抱えていると、そのどれかひとつを使っただけでもファイル全体が必要になります。逆に、コンポーネントごと、責務ごとにきちんと分かれていれば、未使用部分を落としやすくなります。
component-based packages では、とくに「1 コンポーネント 1 モジュール」に近い構造が有効なことが多いです。これは極端な細分化を意味するのではなく、少なくとも Button を使うために Tabs のコードや重い utility まで同じファイルから読まなくてよい状態を作るということです。つまり、tree shaking のためには、単に export を分けるだけでなく、物理ファイルの責務も素直に分けておく必要があります。
4.1 1 コンポーネント 1 モジュール構成の強み
1 コンポーネント 1 モジュール構成のよいところは、依存関係が追いやすくなることです。Button は Button の実装と、その Button に必要な最小限の helper や style だけを参照する、という形になっていれば、他のコンポーネントが bundle へ巻き込まれにくくなります。利用側が Button だけを import したとき、bundler も Button 周辺だけを残しやすくなります。
また、この構成は保守面でも有利です。コンポーネント単位で責務が見えやすく、どこで副作用が起きるかも把握しやすくなります。つまり、1 コンポーネント 1 モジュールは tree shaking のためだけの構造ではなく、部品の境界を明確にし、設計を素直に保つための構造でもあります。
4.2 index.ts の設計を雑にしない
多くのパッケージでは、最終的に index.ts が公開入口になります。しかし、この index が何でも再 export する巨大なハブになってしまうと、内部で不必要な依存がつながりやすくなります。たとえば、全コンポーネントを export するつもりが、ついでに theme 初期化や global CSS や icons 全体まで読み込んでしまうような構造は、tree shaking を弱めやすいです。
そのため、index.ts は「全部をひとまとめにする場所」ではなく、「公開してよい入口だけを整理する場所」として扱うほうがよいです。必要ならサブパスごとに入口を分け、トップレベル barrel には軽い参照だけを置くのも有効です。つまり、index.ts を雑に作ると、パッケージ全体の tree shaking 設計も雑になりやすいです。
4.3 util・style・component を密結合にしない
コンポーネント実装でよくあるのが、shared utility、style helper、theme logic などを深く結びつけすぎることです。たとえば、Button ひとつのために巨大な theme module や全体 utility を必ず通るような構造だと、Button だけ使いたくても、その大きな依存が bundle に残りやすくなります。密結合な構造は tree shaking にとってかなり不利です。
そのため、utility や style は、本当にそのコンポーネントが必要とする最小限にとどめ、全体依存を避けることが重要です。shared に見えるコードでも、「全体でひとつにしたほうがきれい」だけでまとめると、結果として小さく削れないパッケージになりやすいです。つまり、共通化と削りやすさのバランスを意識したファイル構成が必要です。
5. sideEffects 設定をどう考えるべきか
package.json の sideEffects フィールドは、tree shaking を語るときによく出てくる重要な設定です。これは「このパッケージのファイル群は、未使用なら安全に消してよいか」を bundler に伝えるための手がかりになります。もし副作用がないなら、未参照モジュールを積極的に落としやすくなります。一方で、副作用のあるファイルが含まれているなら、そのファイルは注意深く扱う必要があります。つまり、sideEffects は最適化のための小技ではなく、配布物の性質を bundler へ正しく伝えるための宣言です。
ただし、この設定は雑に扱うと危険です。全部 false にすれば tree shaking が強く効くように見えますが、実際には CSS import や global registration、polyfill 読み込みのような副作用が存在することもあります。その場合、本来必要なコードまで消えてしまう可能性があります。つまり、sideEffects は「最適化を強くするスイッチ」ではなく、「何が安全に落とせて、何が落とせないかを正確に伝える責任ある設定」と考えるべきです。
5.1 package.json の sideEffects フィールドの意味
sideEffects: false は、「このパッケージ内のモジュールは、import されても export 以外の副作用を持たない」という前提を bundler へ伝える強いシグナルです。これにより、未使用 import をより大胆に除去しやすくなります。tree shaking を効かせたい component-based packages では非常に有効なことがあります。
しかし、これはあくまで前提が正しい場合に限ります。もしモジュール読み込み時に CSS を適用したり、custom elements を自動登録したり、グローバル変数を変更したりするなら、そのモジュールは「純粋な export だけ」ではありません。つまり、sideEffects: false は便利ですが、コード実態と一致していなければ危険な宣言になります。
5.2 CSS import がある場合の注意点
コンポーネントと CSS を同じモジュール内で import している場合、その CSS 読み込みをどう扱うかは慎重に考える必要があります。利用側にとっては「Button を import したら Button の style も入る」のは便利ですが、bundler から見ると CSS import は副作用とみなされやすいです。そのため、sideEffects を全部 false にすると、必要な style まで落ちる危険があります。
この問題を避けるには、CSS を副作用として明示するか、CSS をコンポーネント単位で独立 entry にするか、あるいは build pipeline の想定を明確にする必要があります。つまり、CSS は tree shaking を難しくしやすい領域なので、「コンポーネントコード」と同じ感覚で扱わないほうが安全です。
5.3 本当に副作用があるコードを切り分ける方法
tree shaking に強いパッケージを作るなら、副作用を持つコードはできるだけ明示的に分離したほうがよいです。たとえば、global theme setup、polyfills、registry 追加、icon sprite 注入などは、コンポーネント本体とは別 entry に切り出しておくと、未使用コンポーネントの削除を邪魔しにくくなります。
こうしておくと、通常のコンポーネントモジュールは比較的純粋な export 集合として扱いやすくなり、sideEffects も整理しやすくなります。つまり、副作用をなくせないなら、せめて「どこにあるのか」を明確にしておくことが大切です。
{
"name": "@acme/ui",
"type": "module",
"sideEffects": [
"**/*.css",
"./dist/register-all.js"
]
}
6. コンポーネント実装で避けたいアンチパターン
tree shaking の効き方は、配布設定や export 設計だけでなく、コンポーネントの実装スタイルにも大きく左右されます。とくに問題になりやすいのは、「import した瞬間に何かが実行される」ような書き方です。コンポーネントというと見た目の部品として捉えられがちですが、モジュールレベルで副作用を持っていると、そのファイルはもはや「ただの部品定義」ではありません。未使用 export を落としたくても、bundler はその副作用まで安全に無視できるとは限らないため、結果として不要コードが残りやすくなります。
また、コンポーネント単体は軽く見えても、裏側で巨大な shared dependency を自動的に読み込んでいたり、全コンポーネント共通の初期化ロジックへ結びついていたりすると、使っていない sub-module まで bundle に入りやすくなります。つまり、tree shaking に強い component-based packages を作るには、「import したらすぐ動く便利設計」より、「必要なときだけ明示的に使う設計」のほうが向いている場合が多いです。
6.1 import した時点で実行される初期化処理
モジュールトップレベルで初期化処理を走らせる書き方は便利に見えます。たとえば、テーマを自動登録したり、グローバル設定を差し込んだりすることで、利用側は余計な手順なしに部品を使えるようになります。しかし、その便利さの代わりに、そのモジュールは「読むだけで副作用が起きるファイル」になります。tree shaking の観点ではこれは不利です。
そのため、初期化が必要なら、明示的な関数呼び出しへ分離したほうがよいことが多いです。コンポーネント import と、初期化処理を実行することを分けておけば、未使用コンポーネントまで副作用のせいで残る状況を防ぎやすくなります。つまり、便利な自動初期化ほど、配布パッケージでは慎重に扱うべきです。
6.2 グローバル登録や自動 setup の埋め込み
Web Components 系のライブラリでは、import しただけで custom element を登録するような設計を見ることがあります。また、icon library では sprite 注入、style system では global style 適用なども起こりやすいです。これらは利用者にとって便利ですが、モジュール単位で副作用を強く持つため、tree shaking には不利になりやすいです。
より安全なのは、「registerAll()」や「installGlobalStyles()」のような明示的 entry を別に用意し、通常のコンポーネント import とは切り分けることです。こうしておくと、通常使用では純粋な component exports を保ちやすくなります。つまり、自動 setup を埋め込まないことは、tree shaking だけでなく API の責務分離にもつながります。
6.3 使っていない sub-module まで巻き込む設計
Button ひとつを使うために、内部で icon registry、theme runtime、全体 helper、analytics hook まで読み込んでいるような設計は、component-based packages ではかなり危険です。一見すると共通化されていて美しく見えるかもしれませんが、利用側から見ると「小さな部品を取ったつもりが大きな依存をまとめて引き込んでいる」状態です。
この問題は、shared code の設計が大きすぎると起こりやすいです。共通化は大切ですが、「本当に全コンポーネントから必要か」を疑わずにまとめてしまうと、tree shaking が効きにくくなります。つまり、使っていない sub-module まで巻き込まないためには、shared dependency の責務を小さく保つ必要があります。
// avoid
import "./register-icons";
import "./install-theme";
import { heavyHelper } from "../core/heavy-helper";
export function Button() {
return `<button>OK</button>`;
}
7. 依存関係の持ち方で bundle size はどう変わるか
component-based packages の bundle size は、各コンポーネント自体の大きさだけでなく、それらが依存している shared modules の構造にも強く影響されます。とくにありがちなのは、「共通化のため」に作った巨大 utility が、実際にはほぼ全コンポーネントから参照される状態になることです。共通化はコード重複を減らすには有効ですが、bundle の観点では「全部の部品に大きな共通依存がぶら下がる」構造を作りやすいです。つまり、依存関係は DRY だけで評価してはいけません。
また、外部依存の選び方でも bundle size はかなり変わります。小さな機能のために大きな utility library 全体を取り込んでいたり、tree shaking しにくい外部パッケージへ依存していたりすると、自分たちのパッケージがいくらきれいでも最終 bundle は重くなります。つまり、component-based packages では「自分のコードをどう削るか」だけでなく、「何を依存として抱えるか」まで含めて設計しなければなりません。
7.1 shared utility を巨大化させない
shared utility は便利ですが、すべての helper をひとつの巨大モジュールへまとめてしまうと、その一部だけが必要なコンポーネントでもモジュール全体へ依存しやすくなります。理論上は tree shaking で削れるように見えても、内部で side effect や複雑な参照があると、思ったほどきれいには落ちません。結果として、「共通 helper を入れた時点で重くなる」状態が起きやすくなります。
そのため、shared utility も責務単位で分けておくほうがよいです。format 用 helper、DOM helper、style helper、accessibility helper など、必要なものだけを最小単位で import できるほうが、結果として bundle も小さくしやすいです。つまり、共通化は大切ですが、「小さく共通化する」意識が重要です。
7.2 1 つの helper が全コンポーネントへ波及する問題
「どのコンポーネントもだいたい使うから」と思って作った helper が、実際には巨大な依存の入口になってしまうことがあります。たとえば、class name 生成 helper のつもりが theme 解決や runtime style 挿入まで含むようになれば、その helper を使う全コンポーネントが重くなります。しかも、その重さは import 文では見えにくいため、問題が発見されにくいです。
この種の問題を避けるには、helper を「一見便利な統合入口」にしすぎないことが大切です。使う側からすると短い import で済んでいても、内部では巨大な処理系につながっているなら、tree shaking の観点ではかなり不利です。つまり、共通 helper は「使いやすい入口」だけでなく、「どれだけ小さく分離されているか」も重要です。
7.3 external dependencies の扱いを見直す
自分たちのコードがどれだけきれいでも、依存している external package が重かったり、tree shaking に弱かったりすると、最終 bundle size は簡単に大きくなります。とくに icon ライブラリ、date utility、class name utility、深い polyfill 依存などは要注意です。小さな機能のために大きな package 全体を引き込んでいるケースは、実務ではかなりよくあります。
そのため、component-based packages では「どの package を使うか」だけでなく、「どの import path で使うか」まで気を配ったほうがよいです。部分 import ができるのか、ESM を持っているのか、side effects は明確か、といった点まで見て依存を選ぶと、後からの bundle 最適化がかなり楽になります。
8. build ツールごとの挙動の違いを理解する
tree shaking を語るとき、「うちは Rollup だから大丈夫」「webpack がやってくれるはず」と考えたくなることがあります。しかし、実際には bundler ごとに得意なことと注意点があり、同じパッケージでも build tool によって結果が変わることがあります。つまり、tree shaking は標準仕様ではなく、各ツールが ESM や side effects 情報をどう解釈して最適化するかに依存する部分があります。そのため、配布側は特定の bundler だけを前提にせず、できるだけ広く安全に削りやすい構造を作る必要があります。
また、利用側の環境が常に同じとは限らないことも重要です。自分たちのライブラリを使う側は Rollup かもしれないし、webpack かもしれないし、Vite / esbuild 系かもしれません。そのため、「自分の build で小さかった」だけでは不十分で、「一般的な利用環境でも削れやすいか」を考える必要があります。つまり、build tool の違いを理解することは、パッケージ配布者にとってかなり実務的なテーマです。
8.1 Rollup での tree shaking の特徴
Rollup は ESM ベースの静的解析に非常に強く、tree shaking と相性がよいことで知られています。component-based packages を build する側としても、Rollup は出力の分かりやすさや module graph の扱いやすさという点で魅力があります。純粋な ESM モジュール構造であれば、かなり素直に未使用 export を落としてくれます。
ただし、Rollup でも副作用が曖昧だったり、巨大な barrel や動的 import 構造が複雑だったりすると、思ったほど削れないことがあります。つまり、Rollup は強力ですが、設計が悪ければ自動で救ってくれるわけではありません。Rollup だから安心、ではなく、Rollup が理解しやすい構造を作る意識が必要です。
8.2 webpack で注意すべき設定
webpack でも tree shaking は可能ですが、production mode や used exports、sideEffects 情報など、いくつかの条件が整って初めて効果を発揮します。利用側設定に影響される部分もあるため、配布側は package.json や exports 設計をより丁寧にしておいたほうが安全です。
また、webpack 環境では CSS や asset 周りの扱いが build pipeline と密接に結びつくことが多く、そこで side effects が曖昧になると tree shaking の効果が見えにくくなることがあります。つまり、webpack 向けには「コード本体」だけでなく、「style や asset をどう混ぜているか」も重要です。
8.3 Vite / esbuild 系で見落としやすい点
Vite や esbuild 系のツールは非常に高速で、ESM ベースのワークフローと相性がよいです。そのため、「最近のツールだから tree shaking も完璧だろう」と思われやすいですが、やはり元のパッケージ設計が悪ければ限界があります。速く build できることと、細かく未使用コードを安全に落とせることは別問題です。
また、開発中の見え方と production build 結果が違うこともあるため、dev server で軽く見えても最終 bundle は別途確認する必要があります。つまり、Vite / esbuild 系でも、「ツールが新しいから大丈夫」ではなく、最終出力で本当に削れているかを見る姿勢が大切です。
9. CSS・アイコン・スタイルシステムはなぜ tree shaking を難しくするのか
component-based packages で tree shaking を難しくしやすい要素として、CSS とアイコンとスタイルシステムがあります。これらは UI ライブラリにとって重要ですが、コード以上に副作用や密結合が起きやすい領域でもあります。たとえば、Button ひとつを import しただけで global CSS 全体が読み込まれる、1 個のアイコンのために icon set 全部が bundle に残る、といったことは珍しくありません。つまり、UI ライブラリの bundle が重い原因はコンポーネントコードそのものではなく、周辺の styling / assets にあることも多いです。
とくに CSS は、JavaScript のように「この export だけ使っているからここだけ残す」という発想と相性が悪い部分があります。また、アイコンは便利な一括入口を作るほど大量の SVG や metadata を巻き込みやすくなります。つまり、component-based packages の tree shaking を本気で考えるなら、コンポーネント本体だけでなく、CSS とアイコンの設計も同じくらい重要だと理解しておく必要があります。
9.1 コンポーネント単位で style を分離する考え方
コンポーネントごとの style を適切に分離しておくと、使っていない部品の style まで bundle に入る状況を避けやすくなります。逆に、全コンポーネントの CSS をひとつの巨大ファイルへまとめてしまうと、利用側が Button しか使わなくても Modal や Table の style まで持っていくことになりがちです。
もちろん、一枚の CSS にまとめる運用が便利な場面もありますが、配布パッケージとして tree shaking を意識するなら、少なくともコンポーネント単位で style 責務を分けられる構造のほうが有利です。つまり、style 設計も component boundary に合わせて考えるべきです。
9.2 icon package が肥大化しやすい理由
icon 系はとくに tree shaking を難しくしやすい領域です。ひとつの入口から大量のアイコンを export していると、利用側が 1 個しか使っていなくても、構造次第では多くのアイコン情報が bundle に残ることがあります。これは named export されていても、内部 barrel や共通 registry が重い場合には起こりえます。
そのため、アイコンは「使いやすい一括 API」だけでなく、「個別 import しやすい構造」も持っていたほうがよいです。たとえば @pkg/icons/arrow-left のようなサブパスを用意しておくと、利用側が明示的に小さく使いやすくなります。つまり、icon package は component library 以上に tree shaking 設計が重要です。
9.3 theme file と runtime style の切り分け
テーマや runtime styling を持つ design system では、theme resolution や style injection がコンポーネントの中へ深く入り込みやすいです。これが便利な一方で、Button を使うために theme runtime 全体を常に読み込むような構造だと、tree shaking しづらくなります。
そのため、theme file と runtime logic を必要以上に一体化させないことが重要です。トークン定義、計算ロジック、runtime injection を分け、必要なレベルだけを各コンポーネントへ渡すほうが削りやすくなります。つまり、スタイルシステムの設計も bundle efficiency の観点から見直すべきです。
10. 実際に tree shaking が効いているかをどう確認するか
tree shaking の議論では、「理論上は削れるはず」という話が多くなりがちです。しかし、実務で大切なのは、実際に build 結果でどうなっているかです。ESM で出していても、index 設計や CSS の混ざり方、依存関係の持ち方によって結果は大きく変わります。そのため、「こう書けば tree shaking されるはず」と思い込むのではなく、bundle analyzer や build 比較によって検証することが不可欠です。つまり、tree shaking は信じるものではなく、確認するものです。
また、利用パターンを変えて比較してみることも重要です。トップレベル import とサブパス import で bundle size がどう変わるのか、Button だけ使った場合と Button + ThemeProvider を使った場合でどれくらい差があるのか、といった検証をしておくと、設計のどこが重いのかが見えてきます。つまり、tree shaking の改善は build 結果を見ながら進めるべきで、理論だけで終わらせない姿勢が大切です。
10.1 bundle analyzer を使った確認方法
bundle analyzer を使うと、最終 bundle の中にどのモジュールがどれくらいのサイズで残っているのかを可視化できます。これによって、「Button しか使っていないのに icons 全体が残っている」「theme runtime が思った以上に大きい」といった問題を具体的に発見しやすくなります。tree shaking は結果が見えにくい最適化なので、こうした可視化ツールの価値は非常に大きいです。
また、analyzer は問題発見だけでなく、改善の優先順位付けにも役立ちます。理論上怪しいと思っていた場所より、実際には別の dependency が支配的だったということも珍しくありません。つまり、bundle analyzer は tree shaking 設計を勘ではなく根拠で進めるための基本ツールです。
10.2 import パターンを変えて比較する
同じパッケージでも、import { Button } from "@pkg/ui" と import { Button } from "@pkg/ui/button" で結果が変わることがあります。これは export 設計や barrel の構造によるものです。つまり、tree shaking の確認では「ひとつの import パターンだけ」で判断しないほうがよいです。
複数の利用パターンを想定して build 比較をしておくと、自分たちの配布設計がどの使われ方に強いのか、どこで無駄が出るのかが分かりやすくなります。つまり、利用側の視点で import パターンを試すことが、実務的な検証には欠かせません。
10.3 理論ではなく build 結果で判断する重要性
「ESM だから大丈夫」「sideEffects を書いたから削れるはず」という説明だけで安心するのは危険です。最終的に意味があるのは、利用アプリケーションの build で実際に bundle がどうなっているかです。理論と実際の build 結果が一致しないことは珍しくありません。
そのため、tree shaking は原理を理解しつつも、最後は build artifact で判断する姿勢が必要です。つまり、最適化は推測ではなく観測で進めるべきです。これができると、改善もかなり現実的になります。
11. 配布パッケージとしてのベストプラクティス
component-based packages を tree shaking に強い形で配布するには、いくつかの基本原則があります。第一に ESM-first であること、第二に exports フィールドを明確にし、利用側が意図した entry を解決しやすいこと、第三にコンポーネント単位の entry point を必要に応じて持てることです。これらは別々の話に見えますが、実際には「利用側が必要なものだけを選べる配布物にする」という一点でつながっています。
さらに重要なのは、API の使いやすさと bundle efficiency を両立させることです。トップレベル import が便利でも、それで tree shaking が極端に弱くなるなら問題ですし、逆にサブパスだらけで使いにくくなっても設計としては偏りすぎです。つまり、ベストプラクティスとは単に削れる構造を作ることではなく、「使いやすくて、しかも削りやすい」というバランスを取ることにあります。
11.1 ESM-first で配布する
ESM-first 配布は、現代の component-based packages ではかなり基本に近い考え方です。利用側 bundler が静的解析しやすくなり、tree shaking の前提条件も整えやすくなります。CommonJS を補助的に残す場合でも、少なくともメインの利用経路として ESM を選ばせやすい設計にしておいたほうがよいです。
これは単に「時代に合っているから」ではなく、未使用コード削除のしやすさが大きく変わるからです。つまり、ESM-first は配布モダン化というより、bundle optimization の土台です。
11.2 exports フィールドを明確に設計する
exports フィールドを明確にしておくと、利用側がどの entry を参照すべきかがはっきりし、深すぎる内部パス依存も避けやすくなります。トップレベル、サブパス、types の関係をきれいに設計しておくと、利用体験も安定しやすくなります。
また、exports が曖昧だと、想定していないファイルが直接 import されたり、CJS 側へ流れたりすることがあります。これは tree shaking の効き方にも影響します。つまり、exports 設計は DX のためだけでなく、bundle への影響を制御するためにも重要です。
11.3 component ごとの entry point を用意する意義
トップレベル barrel だけでなく、コンポーネントごとの entry point も持っておくと、利用側は必要に応じてより明示的に小さく import しやすくなります。すべての利用者がそうするわけではありませんが、「必要なら細かく取れる」状態を作っておくのは有効です。とくに icons や heavy widgets のようにサイズ差が大きい部品では意味があります。
これは API を増やすことでもありますが、component-based packages の本質は「小さな部品の集合」である以上、配布入口もある程度その単位へ対応していたほうが自然です。つまり、component ごとの entry は tree shaking を補助するだけでなく、利用者に bundle control の余地を渡す設計でもあります。
{
"name": "@acme/ui",
"type": "module",
"exports": {
".": "./dist/index.js",
"./button": "./dist/button.js",
"./modal": "./dist/modal.js",
"./tabs": "./dist/tabs.js"
}
}
12. 長期運用で tree shaking に強いパッケージを作るには
tree shaking に強いパッケージは、1 回の build 調整だけで完成するものではありません。最初は小さく始めても、コンポーネントが増え、テーマ機能が増え、icons や utility が追加され、利用アプリも増えていくと、少しずつ重くなっていきます。そのため、本当に重要なのは「今うまく削れているか」だけでなく、「今後大きくなっても削りやすさを保てる構造か」です。つまり、tree shaking に強いパッケージとは、現在の最適化結果だけでなく、成長に耐えられる設計を持っているパッケージのことです。
長期運用では、開発体験のための便利機能がどんどん追加されやすいです。barrel を増やす、global setup を自動化する、共有 helper を統合する、icons を一括 export する、といった判断は、短期的には便利です。しかし、それが積み重なると、気づかないうちに「何を使っても全体が重い」パッケージになってしまいます。だからこそ、定期的に bundle を測定し、side effects を点検し、共通依存の肥大化を疑い、exports 設計を見直すことが必要です。つまり、tree shaking は build 時の最適化オプションではなく、パッケージ運用の品質指標として扱うべきです。
12.1 API 設計と bundle efficiency を両立させる
使いやすい API は重要ですが、その便利さのために内部が大きな束になりすぎると、component-based packages の価値は下がります。逆に、削りやすさだけを優先して import path を細かくしすぎると、利用側が使いにくくなります。つまり、API 設計では「書きやすさ」と「削りやすさ」を同時に考える必要があります。
理想は、トップレベル import でもある程度削れ、さらに必要なら subpath で明示的に小さく使える構造です。これは少し手間がかかりますが、長期的には利用者にも配布者にもメリットがあります。つまり、bundle efficiency は DX の敵ではなく、うまく設計すれば共存できます。
12.2 開発体験だけでなく配布後の消費体験も考える
パッケージを作る側は、どうしても自分たちの開発体験を基準に設計しがちです。便利な re-export、自動 setup、統合 helper はその典型です。しかし、component-based packages は「作る側」より「使う側」がずっと長く触るものです。そのため、配布後の消費体験、つまり import のしやすさ、bundle size、ビルド結果まで考えた設計が必要です。
利用側から見れば、「数個のコンポーネントを使っただけで巨大な bundle になる」のは、内部事情に関係なく使いづらいパッケージです。つまり、ライブラリは書く体験だけでなく、消費される体験まで設計対象に含めるべきです。tree shaking のしやすさは、その消費体験の一部です。
12.3 design system を育てるときに意識したいこと
design system は時間とともに成長し、部品も機能も増えていきます。最初は数個のコンポーネントでも、やがて dozens、hundreds という規模になることがあります。そのとき、最初期の「雑でも困らなかった設計」がそのまま重荷になることがあります。barrel が太り、shared dependency が巨大化し、theme runtime がすべてへ波及し、icons が肥大化する、といったことはよくあります。
だからこそ、design system を育てるなら、初期から tree shaking に強い境界を意識しておくことが大切です。副作用は分ける、component ごとの entry を考える、shared を肥大化させない、build 結果を継続的に測る、といった地道な設計が後で大きな差になります。つまり、tree shaking は大規模化してから慌てて対処するものではなく、成長前提の design system なら早い段階から品質項目として持っておくべきです。
13. おわりに
component-based packages における tree shaking は、単に build tool の設定を入れることではありません。ESM 配布、export 設計、ファイル構成、sideEffects、CSS や icons の扱い、shared dependency の切り方、exports の公開方針まで含めて、パッケージ全体の設計として考える必要があります。つまり、tree shaking は「最後に効かせる最適化」ではなく、「最初から削りやすく作る設計」の問題です。ここを理解すると、bundle size の問題は build 時の偶然ではなく、構造の結果として捉えられるようになります。
本当に tree shaking に強いパッケージとは、必要なものだけを自然に import でき、未使用コードを bundler が無理なく落とせるパッケージです。そして、そのためには API のきれいさと内部構造の素直さの両方が必要です。component-based packages は本来、小さな部品を組み合わせて使うためのものです。だからこそ、配布されるコードも「小さく使えること」を前提に設計されているべきです。それができているパッケージほど、利用側にとっても実務で扱いやすく、長く信頼される基盤になっていきます。
次はこのまま 「full code examples 多め版」 にして、package.json、exports、Rollup config、Button component までつなげた実装寄りの版にもできます.
yes
Thought for 18s
了解です。
下は、前の内容をそのまま実装寄りに広げた版です。
つまり、component-based packages で tree shaking を効かせるための実践構成を、package.json、exports、sideEffects、Rollup config、Button component、利用側 import 例までつなげて見られる形でまとめています。
メタタイトル
コンポーネントベースのパッケージで tree shaking を効かせる実装方法とは?package 設計から build 設定まで解説
メタディスクリプション
component-based packages で tree shaking を正しく機能させるために、package.json、exports、sideEffects、Rollup 設定、CSS 分離、component 実装、利用側 import パターンまでをコード例付きで詳しく解説します。
slug
tree-shaking-for-component-based-packages-with-code
0. はじめに
component-based packages で tree shaking を効かせたいと考えたとき、多くの人はまず bundler の設定を触ろうとします。もちろん build 設定は重要ですが、実際にはそれだけで解決することはほとんどありません。tree shaking が効くかどうかは、配布形式、export 設計、ファイル構成、副作用の切り分け、CSS の扱い、shared dependency の粒度といった、パッケージそのものの構造に強く依存するからです。つまり、tree shaking は build 時のテクニックというより、配布物をどう設計するか という問題として考えたほうが実務に合っています。
特に UI ライブラリや design system のような component-based packages では、利用側が必要とするのはライブラリ全体ではなく、その一部だけであることがほとんどです。Button だけ使いたい画面もあれば、Modal と Input だけ欲しい画面もあります。そのとき、使っていない Tabs や Table、icons 全体、theme runtime 全体まで bundle に残ってしまうと、共通化の利点よりコストのほうが目立ちやすくなります。だからこそ、必要なものだけが自然に残り、不要なものは素直に落ちる構造 を最初から作っておくことが重要です。
1. tree shaking に強いパッケージの前提条件
tree shaking を効かせたいなら、まず前提として「bundler が静的に解析しやすい構造」である必要があります。もっとも基本になるのは ESM で配布することです。どの export がどこで使われているかを追いやすく、未使用コードを落としやすいからです。ただし、ESM にしているだけで自動的にうまくいくわけではありません。モジュールを import した時点で副作用が走る、巨大な barrel export がすべてをまとめて参照している、コンポーネントが重い shared dependency へ密結合している、といった構造があると、ESM でもきれいには削れません。
もう一つ大切なのは、1 コンポーネント 1 責務 に近い構造を保つことです。Button を import しただけで、theme setup、icon registry、global CSS、全 utility を経由するような作りでは、小さく使うことが難しくなります。tree shaking を強くしたいなら、各コンポーネントが必要最小限の依存だけを持ち、不要な初期化や全体参照を避けることが重要です。つまり、最適化は最後に足すものではなく、コンポーネントの境界をどう切るかの時点でかなり決まります。
2. 推奨ディレクトリ構成
実装寄りに考えるなら、まずディレクトリ構成を素直にしておくのが非常に大切です。
下のような構成は、tree shaking と保守性の両方の面で扱いやすいです。
packages/ui/
src/
components/
button/
button.tsx
button.css
index.ts
modal/
modal.tsx
modal.css
index.ts
tabs/
tabs.tsx
tabs.css
index.ts
icons/
arrow-left.tsx
close.tsx
index.ts
theme/
tokens.ts
create-theme.ts
index.ts
utils/
classnames.ts
dom.ts
index.ts
index.ts
dist/
package.json
tsconfig.json
rollup.config.mjs
この構成のよいところは、コンポーネントごとに入口が分かれていて、Button の利用と Modal の利用が物理的にも分離されていることです。さらに、icons、theme、utils の責務もコンポーネント本体から切り離されています。これによって、Button を使うために Tabs のコードを読む必要がなくなり、bundler も依存グラフを追いやすくなります。逆に、すべてを src/index.ts 周辺へ集めすぎると、API はきれいでも内部依存は重くなりやすいです。
3. package.json の基本設計
tree shaking に強い配布物を作るなら、package.json はかなり重要です。
最低限、type、exports、sideEffects は丁寧に設計したほうがよいです。
{
"name": "@acme/ui",
"version": "1.0.0",
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
},
"./button": {
"types": "./dist/components/button/index.d.ts",
"import": "./dist/components/button/index.js",
"require": "./dist/components/button/index.cjs"
},
"./modal": {
"types": "./dist/components/modal/index.d.ts",
"import": "./dist/components/modal/index.js",
"require": "./dist/components/modal/index.cjs"
},
"./tabs": {
"types": "./dist/components/tabs/index.d.ts",
"import": "./dist/components/tabs/index.js",
"require": "./dist/components/tabs/index.cjs"
}
},
"sideEffects": [
"**/*.css"
],
"files": [
"dist"
]
}
ここで重要なのは、トップレベル入口だけでなく、component ごとの subpath export を持っていることです。これにより、利用側は @acme/ui から named import することもできますし、必要なら @acme/ui/button のように明示的に小さく import することもできます。また、sideEffects に CSS だけを明示しているため、「JS モジュール本体は原則削りやすいが、CSS は副作用として保持すべき」という意図を bundler へ伝えやすくなります。
4. exports 設計は API と bundle の両方に効く
exports フィールドは、単なる公開パスの一覧ではありません。利用側がどの入口を通るかをコントロールし、それによって tree shaking の効き方にも影響します。たとえば、トップレベル入口しかない場合、内部 barrel の設計が重いと、それだけで不要な参照が増えやすくなります。逆に、subpath exports があれば、利用側は「この部品だけほしい」という意思をより明確に表現できます。
ただし、subpath を増やしすぎて API が散らかるのも問題です。実務では、@pkg/ui からの named import を基本にしつつ、重い部品や bundle size を厳密に意識する場面では @pkg/ui/button のような明示的 import も使える、という設計がかなり現実的です。つまり、exports 設計では「使いやすさ」と「削りやすさ」の両方を考える必要があります。
// 利用側その1: API が分かりやすい
import { Button, Modal } from "@acme/ui";
// 利用側その2: 必要ならより明示的に小さく使う
import { Button } from "@acme/ui/button";
import { Modal } from "@acme/ui/modal";
5. barrel export の良い例と悪い例
barrel export は便利ですが、設計を間違えると tree shaking の足を引っ張ります。
まずは悪い例です。
// src/index.ts
export * from "./components/button";
export * from "./components/modal";
export * from "./components/tabs";
export * from "./icons";
export * from "./theme";
export * from "./utils";
import "./global.css";
import "./register-all-icons";
この形の問題は、トップレベル入口を読むだけで global.css や icon 登録まで巻き込んでいることです。たとえ Button だけを使いたくても、この入口は「純粋な export 集合」ではなく、import 時副作用を持つモジュールになります。bundler はこうしたファイルを慎重に扱うため、未使用コードを落としにくくなります。
次に、よりよい例です。
// src/index.ts
export { Button } from "./components/button";
export { Modal } from "./components/modal";
export { Tabs } from "./components/tabs";
// ここでは global style や auto registration はしない
// src/components/button/index.ts
export { Button } from "./button";
// src/theme/index.ts
export { createTheme } from "./create-theme";
export { tokens } from "./tokens";
この形では、トップレベル入口が比較的軽く、何を公開しているかも明快です。副作用を持つ処理を分離しているため、Button だけを使う場合も他の重い機能を巻き込みにくくなります。つまり、barrel export 自体が悪いのではなく、barrel に何を入れるか が重要です。
6. side effects は分離して扱う
tree shaking の観点では、「import した時点で何かが起きるコード」は非常に扱いが難しくなります。
たとえば次のようなコードです。
// bad: importしただけで副作用が走る
import { registerIcons } from "../icons/register";
registerIcons();
export function Button() {
return <button>OK</button>;
}
この Button は、未使用であっても import 自体を落とすと icon 登録の動作が変わる可能性があります。そのため bundler は安全に除去しにくくなります。こうした副作用は、コンポーネント本体から切り離したほうがよいです。
// good: 副作用は明示的な entry に分離
export function registerAllIcons() {
// icon registry setup
}
// consumer
import { registerAllIcons } from "@acme/ui/icons/register-all";
registerAllIcons();
このようにしておけば、通常の Button import は純粋な component module として保ちやすくなります。つまり、side effects をなくせないなら、少なくとも「どこにあるのか」を明示的に分けておくことが大切です。
7. CSS の扱いは tree shaking を難しくしやすい
JavaScript の export は比較的静的に解析しやすい一方で、CSS import は副作用として扱われやすいです。
たとえば次のような構造です。
// src/components/button/button.tsx
import "./button.css";
export function Button() {
return <button className="acme-button">OK</button>;
}
これは component 単位で CSS を閉じ込めているため、比較的扱いやすいです。Button を使わなければ Button の CSS も不要である、という構造が見えやすいからです。一方で、すべての CSS を global.css にまとめ、それをトップレベル index で必ず import してしまうと、Button だけ使うケースでも Modal や Tabs の style まで bundle へ残りやすくなります。
そのため、component-based packages では、CSS もできるだけ component boundary に合わせて分けたほうがよいです。もちろん、theme reset や token 定義のように全体で一度だけ必要なものもありますが、それらまで通常コンポーネント入口へ混ぜないことが重要です。つまり、CSS は「まとめたほうが楽」だけで設計せず、利用側が何を使ったときにどの style が必要なのかが分かる構造にしておくべきです。
8. コンポーネント実装は「小さい依存」を意識する
Button のような小さなコンポーネントでも、内部で重い shared helper や theme runtime に依存していると、小さく使えなくなります。
悪い例を先に見ると分かりやすいです。
// bad
import { createRuntimeThemeResolver } from "../../theme/runtime";
import { iconRegistry } from "../../icons/registry";
import { hugeSharedHelper } from "../../core/huge-helper";
export function Button() {
const theme = createRuntimeThemeResolver();
hugeSharedHelper();
iconRegistry.ensureLoaded();
return `<button class="${theme.buttonClass}">OK</button>`;
}
この Button は、一見小さな部品なのに、theme runtime、icons、巨大 helper にまで依存しています。利用側から見れば Button だけを import しているつもりでも、実際にはかなり広い依存を引き込んでいます。
よりよい形は、Button が必要最小限の依存だけを持つことです。
// good
import { cx } from "../../utils/classnames";
import "./button.css";
export interface ButtonProps {
disabled?: boolean;
children?: string;
}
export function Button({ disabled, children = "OK" }: ButtonProps) {
const className = cx("acme-button", disabled && "is-disabled");
return (
<button className={className} disabled={disabled}>
{children}
</button>
);
}
この形では、Button 自身は軽い utility と局所 CSS だけへ依存しています。必要な theme や icon は、利用側が明示的に別途持ち込めるようにしておいたほうが、パッケージとしては削りやすくなります。つまり、コンポーネント実装では「便利だから全部つなぐ」より、「部品として最小限で成立させる」ことが重要です。
9. Rollup 設定の実践例
配布物の生成には Rollup がかなり相性のよい選択肢です。
下はシンプルな構成例です。
import typescript from "@rollup/plugin-typescript";
import postcss from "rollup-plugin-postcss";
import dts from "rollup-plugin-dts";
const input = {
index: "src/index.ts",
button: "src/components/button/index.ts",
modal: "src/components/modal/index.ts",
tabs: "src/components/tabs/index.ts"
};
export default [
{
input,
output: [
{
dir: "dist",
format: "esm",
entryFileNames: "[name].js",
preserveModules: false,
sourcemap: true
},
{
dir: "dist",
format: "cjs",
entryFileNames: "[name].cjs",
exports: "named",
sourcemap: true
}
],
external: ["react", "react/jsx-runtime"],
plugins: [
typescript({ tsconfig: "./tsconfig.json" }),
postcss({
extract: true
})
]
},
{
input: "src/index.ts",
output: [{ file: "dist/index.d.ts", format: "es" }],
plugins: [dts()]
}
];
この設定で重要なのは、トップレベル入口だけでなく、Button / Modal / Tabs の entry も個別に出していることです。これにより、利用側が subpath exports を通じて小さく import しやすくなります。また、external に React を出しているため、ライブラリ本体の bundle に React を抱え込まずに済みます。つまり、Rollup 設定では「ESM を作る」だけでなく、「どの入口単位で出力するか」「何を external にするか」が tree shaking にかなり影響します。
10. 利用側でどう import されるかまで意識する
配布側は「自分たちの build がきれいならよい」と思いがちですが、実際には利用側がどう import するかまで考えたほうがよいです。たとえば、トップレベル入口が便利でも、そこへ heavy modules が混ざっていれば、利用側の bundle は膨らみやすくなります。逆に、subpath entry を用意しておけば、厳密に最小化したいアプリではそれを選べます。
利用側の検証も、tree shaking を確かめるうえでは非常に重要です。
たとえば簡単な Vite app を作って、次のように比較します。
// case 1
import { Button } from "@acme/ui";
// case 2
import { Button } from "@acme/ui/button";
この 2 つで最終 bundle がどう変わるかを analyzer で見ると、自分たちの exports / barrel 設計が本当に意図どおりかを確認しやすくなります。つまり、tree shaking は理論だけではなく、利用側の build 結果 で見るべきです。
11. bundle analyzer で確認する
tree shaking は「効いているはず」で終わらせないことが大切です。
実際に analyzer を使って、どのモジュールが残っているかを見るべきです。
Vite / Rollup 系なら、たとえば次のような plugin を使えます。
import { visualizer } from "rollup-plugin-visualizer";
export default {
// ...
plugins: [
visualizer({
filename: "stats.html",
open: true
})
]
};
これで build 後に stats.html を開けば、Button しか使っていないのに icons 全体が残っていないか、theme runtime が思った以上に大きくないか、といったことを確認できます。とくに component-based packages では、「小さな部品のつもりが shared dependency で重くなっている」ケースが多いため、可視化はかなり重要です。つまり、最適化は勘ではなく観測で進めたほうがうまくいきます。
12. 実務でのおすすめ方針
実務で tree shaking に強い component-based packages を作るなら、方針としてはかなりはっきりしています。
まず、ESM-first で配布すること。
次に、named export 中心 で API を組み立てること。
さらに、component ごとの entry point を必要に応じて持てるようにすること。
そして、side effects をコンポーネント本体から分離すること。
この 4 つを押さえるだけでも、かなり削りやすいパッケージになります。
そのうえで、CSS、icons、theme、shared utilities のような「便利だけれど重くなりやすい領域」を丁寧に扱うことが重要です。つまり、tree shaking に強いパッケージとは、単に bundler 向けにきれいなモジュールグラフを作ることではなく、「利用側が必要な部品だけを自然に持っていける」構造を用意できているパッケージです。component-based packages は本来、小さな部品を組み合わせて使うためのものです。だからこそ、配布物自体も小さく使えることを前提に設計されているべきです。
おわりに
component-based packages における tree shaking は、build tool の設定だけでは決まりません。package.json、exports、sideEffects、ファイル構成、barrel export、CSS、icons、shared dependency の切り方まで含めた、パッケージ全体の設計問題です。つまり、tree shaking を効かせたいなら、最後に build 設定を調整するのではなく、最初から「削りやすい構造」を意識してコンポーネントを作る必要があります。
実務で本当に価値があるのは、「理論上 tree shaking できる」パッケージではなく、「利用側が Button だけを使ったとき、本当に Button 周辺だけが残る」パッケージです。使いやすい API と、小さく使える配布物。その両方を成立させるのが、良い component-based package の条件です。そこまで考えて設計されたライブラリほど、長く使われやすく、複数プロジェクトでも信頼される基盤になっていきます。
EN
JP
KR