Rollup の Tree Shaking が不要コードを除去する仕組み

JavaScript のバンドルツールにおいて、使われていないコードを最終出力から取り除く技術を Tree Shaking と呼びます。Rollup はこの仕組みを中心に設計されたバンドラであり、ESM の静的構造を活かして効率的に不要コードを検出・除去します。

Tree Shaking とは何か

Tree Shaking という名前は、木を揺さぶって枯れ葉を落とすイメージに由来しています。モジュール間の依存関係をツリー構造として捉え、実際に使われていない枝(エクスポート)を落とすことで、最終的なバンドルサイズを削減します。

たとえば、ユーティリティモジュールに 10 個の関数が定義されていても、アプリケーション側で使っているのが 2 個だけなら、残りの 8 個はバンドルに含める必要がありません。Tree Shaking はこの判断を自動で行います。

ESM の静的構造が前提になる

Rollup の Tree Shaking が機能するには、ESM(ES Modules)の静的な import / export 構文が不可欠です。

ESM(静的)

import と export はファイルのトップレベルに書く必要があり、条件分岐の中には置けない。バンドラはコードを実行しなくても依存関係を解析できる。

CommonJS(動的)

require() は関数呼び出しなので、if 文の中や変数に代入した結果で呼び出し先が変わる。実行してみるまでどのモジュールが必要かわからない。

ESM では import 先と export 先がコードの構文解析だけで確定するため、Rollup はモジュールグラフ全体を静的にたどることができます。これが Tree Shaking の出発点です。

Rollup が不要コードを見つけるまでの流れ

Rollup のビルドプロセスは、大きく 3 つのステップで構成されています。

エントリーポイントから import を再帰的にたどり、モジュールグラフを構築する

各モジュールの export のうち、実際に他から参照されているものだけをマークする

マークされなかった export とそれに依存するコードをバンドルから除外する

このアプローチは inclusion-based(含めるものだけを選ぶ)と呼ばれ、Webpack のように全体を含めてから削るアプローチとは逆方向に動きます。Rollup は最初から「使われているもの」だけを拾い集めるため、デフォルトの状態で Tree Shaking が効いているのが特徴です。

具体的なコードで確認する

実際にどのようにコードが除去されるか、簡単な例で見てみましょう。まず、2 つの関数をエクスポートするモジュールを用意します。

// math.js
export function add(a, b) {
  return a + b;
}

export function multiply(a, b) {
  return a * b;
}

エントリーポイントでは add だけを使います。

// main.js
import { add } from './math.js';

console.log(add(2, 3));

Rollup でビルドすると、出力は次のようになります。

// bundle.js
function add(a, b) {
  return a + b;
}

console.log(add(2, 3));

multiply 関数はどこからも参照されていないため、バンドルには含まれません。import 文や export 文もすべて解決済みなので、出力にはモジュール構文が残らず、フラットなコードになります。

Side Effects という落とし穴

Tree Shaking が万能でない理由のひとつが、副作用(Side Effects)の存在です。モジュールのトップレベルに書かれたコードが、import されただけで何らかの影響を外部に与える場合、そのコードは「使われていなくても除去できない」と判断されます。

// analytics.js
export function track(event) {
  // 追跡処理
}

// トップレベルの副作用
window.analyticsLoaded = true;

この例では track 関数を誰も使っていなくても、window.analyticsLoaded = true というトップレベルの実行文があるため、Rollup はこのモジュールを安全に除去できません。グローバルオブジェクトへの代入は外部から観測可能な変化であり、削除するとプログラムの動作が変わる可能性があるからです。

sideEffects フィールドで明示する

この問題に対処するため、package.json に sideEffects フィールドを設定できます。

{
  "name": "my-library",
  "sideEffects": false
}

sideEffects: false と宣言すると、このパッケージのどのモジュールもトップレベルの副作用を持たないことをバンドラに伝えられます。Rollup はこの情報を信頼して、未使用のモジュールを安心して除去できるようになります。

特定のファイルだけ副作用があるなら、配列で指定することも可能です。

{
  "name": "my-library",
  "sideEffects": ["./src/polyfill.js", "*.css"]
}

この設定では polyfill.js と CSS ファイルだけが副作用ありとして扱われ、それ以外のモジュールは安全に除去対象となります。

Rollup と Webpack の Tree Shaking の違い

同じ Tree Shaking でも、Rollup と Webpack ではアプローチが異なります。Rollup は ESM をネイティブに扱う設計であり、モジュールグラフの構築時点で不要なエクスポートを含めません。一方、Webpack は元々 CommonJS を中心に発展してきたバンドラで、Tree Shaking は後から追加された機能です。

Webpack では Terser などのミニファイアがバンドル後に未使用コードを除去する二段階方式を取りますが、Rollup ではバンドル生成の段階で不要コードがすでに排除されています。

コードの圧縮・難読化・デッドコード除去を行うツール。UglifyJS の後継として広く使われている。

この設計の違いにより、Rollup はライブラリのビルドに特に向いています。ライブラリはアプリケーションに組み込まれる側なので、未使用部分がきれいに除去できることが利用者にとって大きなメリットになるためです。

Tree Shaking を最大限に活かすコツ

Rollup の Tree Shaking を効果的に機能させるには、コードの書き方にも注意が必要です。

名前付きエクスポートを使う

default export よりも named export のほうが、バンドラが個別の参照を追跡しやすくなります。export default { add, multiply } のようにオブジェクトでまとめると、個別の追跡が難しくなることがあります。

トップレベルの副作用を避ける

モジュールの読み込み時に実行されるコード(グローバル変数の書き換え、DOM 操作など)は Tree Shaking を阻害します。副作用は関数の中に閉じ込め、明示的に呼び出す設計にしましょう。

バレルファイルに注意する

index.js で全モジュールを re-export するバレルパターンは便利ですが、副作用のあるモジュールが混在すると Tree Shaking の妨げになります。sideEffects フィールドとの組み合わせが重要です。

Tree Shaking はバンドラだけの仕事ではなく、コードを書く側の設計判断と組み合わさることで本来の効果を発揮します。ESM の静的構造を意識し、副作用を最小限に抑えたモジュール設計を心がけることが、軽量なバンドルへの近道です。