Rollup(ロールアップ)は、ES Modules(import/export)で書いたJavaScriptを、配布しやすい1つまたは複数のファイルにまとめるモジュールバンドラーです。使われていないexportを出力から外す「ツリーシェイキング」を広めたツールでもあります。npmのパッケージ名はそのまま rollup で、2026年10月12日時点の最新版は10月10日公開の4.64.5です。この記事では、ツリーシェイキングでコードが消える条件と消えずに残る原因、sideEffects や treeshake の設定から、npmでの導入、CLIとrollup.config.mjsでのビルド、主要プラグイン、Webpack・Vite・Rolldownとの使い分けまでを扱います。導入手順の章のコマンド、エラーメッセージ、バイト数は2026年9月27日に node v26.5.0・npm 11.17.0・rollup 4.63.5 で実行して取得した値です。ブロックチェーンの「ロールアップ」や英単語のrollupは扱いません。
まとめ:Rollupのツリーシェイキングとnpm導入・使い方の要点
- Rollupが得意なのはライブラリの配布物づくりです。ESMを静的に解析し、使われていないexportを出力から外します(ツリーシェイキング)。既定で有効なので、設定を書かなくても効きます。
- ツリーシェイキングで消えないコードの原因は、ほぼ次の3つです。副作用があると判定されたモジュール、CommonJS形式の依存、副作用の有無を判定できない関数呼び出し。それぞれ
sideEffects、ESM版への切り替え、#__PURE__注釈で対処します。 - Rollup本体はpackage.jsonの
sideEffectsを読みません。読むのは@rollup/plugin-node-resolveです。プラグインを使わない構成ではtreeshake.moduleSideEffectsで指定します。 - 導入は
npm i -D rollupでプロジェクトに入れ、npm scriptsからrollup -cを呼びます。現行の4.x系はNode.js 18以上が必要です。設定ファイル名はrollup.config.mjsにしておくのが確実です。 - CommonJS版lodashはツリーシェイキングが効かず、debounceを1つ使うだけで578,775バイトになりました(lodash-esなら13,258バイト)。
- Vite 8(2026年3月12日)は本番ビルドのバンドラーをRollupからRolldownへ置き換えました。アプリ開発はVite、npmライブラリの配布やVite以外のビルドはRollup、と分けて考えるのが現実的です。
- 2026年9月時点では、npmの既定で入るBabel 8とTypeScript 7がRollup公式プラグインの対応範囲外です。どちらも一つ前のメジャー版に固定すると動きます。
Rollupとは:ES Modules前提のJavaScriptモジュール結合と配布
公式ドキュメントはRollupを「a module bundler for JavaScript which compiles small pieces of code into something larger and more complex, such as a library or application」と説明しています。小さなモジュールをライブラリやアプリケーションの形にまとめる道具、という意味です。Svelteの作者Rich Harrisが開発し、現在はLukas Taegert-Atkinsonが保守しています。ソースはGitHubの rollup/rollup で公開され、ライセンスはMITです。読み方は「ロールアップ」で、英語のroll upが持つ「ひとまとめにする」という意味が、複数のモジュールを1つにまとめる動作と重なります。
WebpackがCSSや画像も含めたアプリ全体を扱うのに対し、Rollupの守備範囲はJavaScriptの結合と出力形式の変換です。CSSやTypeScriptはプラグインを足して扱います。
ESMの静的解析でツリーシェイキングが未使用のexportを消す仕組み
ESMのimport/exportは実行前に依存関係が確定するため、どの関数が実際に使われているかをバンドル時に判定できる仕組みです。ESMという仕様そのものの位置づけはECMAScriptの版とESMの解説で整理しています。次の例では、math.jsが2つの関数をexportし、main.jsは add だけをimportしています。
// src/math.js
export const add = (a, b) => a + b;
export const unused = () => 'never bundled';
// src/main.js
import { add } from './math.js';
export const run = () => add(1, 2);
これをバンドルした出力を grep -c unused で調べると0件で、unused は出力に含まれません。この判定はESMだからできることで、実行時に読み込み先が決まるCommonJSの require では同じ精度で削れません。どれくらい差が出るかは後半の実測章で示します。
Rollup 4の最新版4.64.5とNode.js 18以上などの動作要件
| 項目 | 値(2026年10月12日時点) |
|---|---|
| 最新版 | 4.64.5(2026-10-10公開) |
| 4.0.0の公開日 | 2023-10-05 |
| 必要なNode.js | 18.0.0以上(engines) |
| 構文解析 | OSごとのネイティブバイナリ |
| ネイティブ非対応の環境 | @rollup/wasm-node(同じAPI) |
| 出力形式の既定値 | es |
| treeshakeの既定値 | true(有効) |
4.x系は9月24日の4.63.5から10月10日の4.64.5まで、約2週間で7回の更新が出ています。パッチ版の更新が速いので、package-lock.jsonで版を固定しておくと、CIと手元で出力が食い違いにくくなります。
4.0.0では、Node.js向けのビルドがOSとCPUごとのネイティブバイナリに依存する形に変わりました。npmは該当するパッケージをoptionalDependenciesとして入れ、Intel Macでは @rollup/rollup-darwin-x64 が入りました。同じ4.0.0で inlineDynamicImports・manualChunks・preserveModules は入力側のオプションから削除され、出力側(output)にだけ書けるようになっています。3.x時代の設定を流用するときは、この3つの位置を確認してください。
ツリーシェイキングとは:未使用コードが消える条件とRollupで残る原因
ツリーシェイキングは、importされていないexportをバンドルから外す処理です。ただし「使っていないのに残る」ケースは珍しくありません。Rollupが残すのは、消すと動作が変わるかもしれないと判定したコードです。この章では、その判定の仕組みと、残ったコードを消させる手段を順に見ます。
デッドコード除去とミニファイの違いとツリーシェイキングの名付け元
webpackのTree Shakingガイドは、ツリーシェイキングを「a term commonly used in the JavaScript context for dead-code elimination」と定義し、名前と考え方は「popularized by the ES2015 module bundler rollup」としています。木を揺すって枯れ葉を落とすように、依存の木から使われない枝を落とすという比喩です。
ミニファイ(terserなど)との違いは、判定の単位にあります。ツリーシェイキングはモジュールとexportの依存関係を見て、到達しないコードを出力に入れません。ミニファイは出力済みのコードを文単位で縮め、変数名の短縮や到達しない分岐の削除を行います。両者は役割が違うので、ライブラリのビルドでは併用します。後半のlodashの比較でも、ツリーシェイキング後にterserを掛けて13,258バイトが2,189バイトまで縮みました。
副作用のあるモジュールが残る理由とpackage.jsonのsideEffects指定
モジュールを読み込んだだけでグローバル変数への代入、プロトタイプの拡張、CSSの注入などが起きる場合、そのモジュールには「副作用」があります。Rollupは既定で、何もimportしていないモジュールでも副作用がありうるものとして残します。treeshake.moduleSideEffectsの既定値がtrueだからです。
ライブラリ側が「このパッケージは読み込んだだけでは何も起きない」と宣言する手段が、package.jsonの sideEffects フィールドです。ただし、Rollupの公式ドキュメントには「Rollup itself does not read a package’s sideEffects field.」とあります。読み取るのは @rollup/plugin-node-resolve で、node-resolveのREADMEによると、依存パッケージの値に加えて、ルートパッケージ(自分のプロジェクト)の sideEffects も既定で尊重されます(ignoreSideEffectsForRoot の既定値はfalse)。
{
"name": "my-lib",
"type": "module",
"sideEffects": ["./src/polyfill.js", "*.css"]
}
false を書けばすべてのファイルを副作用なしとして扱わせられますが、CSSのように「読み込むこと自体が目的」のファイルまで消えます。webpackのガイドも、副作用のためだけにimportするCSSはリストに加えないと本番ビルドで落ちると注意しています。配列で書く場合、*.css のように / を含まないパターンは **/*.css と同じ扱いです。
PUREとNO_SIDE_EFFECTSの注釈コメントで関数呼び出しを削らせる書き方
モジュール単位では副作用がなくても、トップレベルの関数呼び出しは残りやすい箇所です。export const registry = createRegistry(); の registry を誰も使っていなくても、Rollupは createRegistry() の中で副作用が起きうると判定すれば呼び出しを残します。これを消させるのが注釈コメントです。treeshake.annotationsの説明によると、@__PURE__ または #__PURE__ を含むコメントは特定の関数呼び出しやコンストラクタ呼び出しを副作用なしとして扱わせ、#__NO_SIDE_EFFECTS__ は関数宣言そのものを副作用なしとして扱わせます。
// src/registry.js
/*#__NO_SIDE_EFFECTS__*/
export function createRegistry() {
return new Map();
}
// 呼び出し単位で指定する場合
export const registry = /*#__PURE__*/ createRegistry();
export const cache = /*#__PURE__*/ new WeakMap();
どちらの書き方でも、registry や cache がどこからも使われなければ呼び出しごと出力から外れます。#__NO_SIDE_EFFECTS__ は関数側に1回書けば呼び出し側すべてに効くので、ファクトリ関数を公開するライブラリでは書き漏れが減ります。注釈を無視させたいときは treeshake.annotations: false にします(既定はtrue)。
注釈は「本当に副作用がない」ことを書き手が保証する宣言です。中でグローバルへの登録やログ出力をする関数に付けると、その処理が本番ビルドだけ消える不具合になります。
ESMがCommonJSへ変換されてツリーシェイキングが効かなくなる失敗例
ツリーシェイキングの前提は、import/exportが持つ静的な構造です。webpackのガイドは結論で、ES2015のモジュール構文をCommonJSへ変換するコンパイラーを通さないことを条件に挙げ、Babelの @babel/preset-env を例に出しています。TypeScriptも、tsconfig.jsonの module を commonjs にしたまま出力をRollupへ渡すと、import文が require に変わった状態でバンドラーに届きます。
依存パッケージ側がCommonJSで配布されている場合も同じです。@rollup/plugin-commonjs はCommonJSをESMへ変換して取り込めるようにしますが、変換後もモジュール全体が1つの値として扱われるため、使わない関数だけを抜く精度は出ません。後半のlodashの比較で約44倍の差が出たのはこのためです。ESMとCommonJSの相互運用の詳細はESMとCommonJSの違いとrequire(esm)による相互運用で扱っています。
treeshakeオプションでRollupのツリーシェイキングを調整する設定例
treeshakeは boolean、プリセット名、詳細オプションのオブジェクトのいずれかを受け取ります。既定値はtrueで、falseにすると「bigger bundles but may improve build performance」、つまりバンドルは大きくなる代わりにビルドが速くなる場合があります。CLIでの指定に使うのは、--treeshake <preset> と --no-treeshake です。
smallest・recommended・safestの3プリセットで変わる削除の範囲
| プリセット | 主にfalseになる項目 | 向く場面 |
|---|---|---|
| smallest | moduleSideEffects ほか4項目 | 副作用のない自前コードのみ |
| recommended | グローバル副作用など2項目(本文参照) | 多くのアプリとライブラリ |
| safest | なし(仕様準拠を優先) | 挙動の差を避けたい移行時 |
recommended は unknownGlobalSideEffects と correctVarValueBeforeDeclaration をfalseにします。smallest でさらにfalseになる項目は、propertyReadSideEffects・moduleSideEffects・tryCatchDeoptimization です。公式ドキュメントは smallest の制約として、副作用のあるgetterは戻り値を使う場合しか残らない、importしたモジュールのコードは少なくとも1つのexportを使う場合しか残らない、壊れた組み込み機能を検出する前提のpolyfillはバンドルすべきでない、の3点を挙げています。
最後の1点は見落としやすい箇所です。tryCatchDeoptimizationの既定値はtrueで、Rollupは既定ではtry文の内側でツリーシェイキングを止めます。例外が投げられるかどうかで機能を検出するコードを守るための措置です。smallest はこの保護を外すので、依存にpolyfillを含むアプリでは選びません。
moduleSideEffectsとmanualPureFunctionsの設定例
プリセットを土台に、個別の項目を上書きできます。次の例は recommended を基準にしつつ、CSSとpolyfillのファイルだけ副作用ありとして残し、それ以外の何もimportされないモジュールは外します。
// rollup.config.mjs
export default {
input: 'src/main.js',
output: { dir: 'dist', format: 'es' },
treeshake: {
preset: 'recommended',
moduleSideEffects: (id, external) =>
!external && (id.endsWith('.css') || id.includes('polyfill')),
manualPureFunctions: ['styled'],
},
};
moduleSideEffects は true・false・'no-external'・副作用のあるモジュールの配列・関数のいずれかを取ります。'no-external' は、可能な場合に外部依存(external)のimportだけを外す指定です。関数は (id, external) => boolean の形で、モジュールごとに判定します。
manualPureFunctions は、名前で指定した関数を常に副作用なしとして扱わせる配列です。判定は関数名だけで行われるので、同じ名前で副作用のある関数が別の場所にあると、それも消えます。CSS-in-JSの styled のように、名前が衝突しにくい関数に限って使います。
設定の効き目は、ツリーシェイキングを止めたビルドと比べると確かめられます。
npx rollup -c -o dist/on.js
npx rollup src/main.js -f es --no-treeshake -o dist/off.js
wc -c dist/on.js dist/off.js
grep -c createRegistry dist/on.js
npmでRollupをインストールしてnpm scriptsから実行する手順
Rollupはプロジェクトごとに開発依存として入れます。グローバルインストールもできますが、プロジェクトによって版がずれるので、CIと手元で同じ版を使うにはローカル導入が確実です。
mkdir my-lib && cd my-lib
npm init -y
npm i -D rollup
npx rollup --version
# rollup v4.63.5(2026年9月27日の実行時)
package.jsonにビルド用のスクリプトを足します。-c は設定ファイルを読む指定、-w はファイル変更を監視して再ビルドするウォッチモードです。
{
"scripts": {
"build": "rollup -c",
"dev": "rollup -c -w"
}
}
npm scriptsから呼ぶ場合、使われる実行ファイルは node_modules/.bin の rollup です。同じ版でビルドするには、package-lock.jsonを共有し、各環境で npm ci により依存を揃えてから npm run build を実行します。pnpmやYarnを使うチームなら、corepackでpnpmなどのパッケージマネージャーの版も固定しておくと、環境ごとの差分が出にくくなります。
CLIでのバンドル手順:es・cjs・iifeなど6つの出力形式とよく使うオプション
設定ファイルを作らなくても、CLIだけでバンドルできます。出力形式は --format(-f)で選び、既定値はesです。
| format | 別名 | 主な使い道 |
|---|---|---|
| es | esm、module | 他バンドラー・モジュールscript |
| cjs | commonjs | Node.jsのrequire向け |
| iife | なし | scriptタグで直接読むアプリ |
| umd | なし | amd・cjs・iifeを1ファイルで兼用 |
| amd | なし | RequireJSなどのローダー |
| system | systemjs | SystemJSローダー |
# ES Modulesで出力(動的importがあればチャンクを分けるので -d で出力先ディレクトリを指定)
npx rollup src/main.js -d dist -f es
# scriptタグ用に1ファイルへまとめる(exportがあれば --name でグローバル変数名を付ける)
npx rollup src/main.js --file dist/app.js --format iife --name App --inlineDynamicImports
# 変更を監視して再ビルド
npx rollup src/main.js -d dist -f es -w
表のes形式の使い道にある「モジュールscript」は、type=moduleを指定したscriptタグを指します。
iifeとumdはチャンク分割に対応していません。コードに import()(動的import)があるまま --format iife を指定すると、Invalid value "iife" for option "output.format" - UMD and IIFE output formats are not supported for code-splitting builds.で止まります。--inlineDynamicImports を付けて1ファイルに畳み込むか、es形式に切り替えてください。
rollup.config.mjs:ESM・CJS同時出力とTypeScript設定例
ESMとCommonJSを同時に出力するライブラリ向けの設定例
ESM版とCommonJS版を両方配る場合は、出力を配列で指定します。次の設定を実行する前に、npm i -D @rollup/plugin-node-resolve @rollup/plugin-commonjs @rollup/plugin-terser lodash-es で使用するプラグインと、例で使うlodash-esを導入してください。入口のsrc/main.jsは、lodash-esのdebounceと前章のmath.jsを使い、heavy.jsを動的importで読み込みます。
// src/main.js
import { debounce } from 'lodash-es';
import { add } from './math.js';
export function run() {
return debounce(() => console.log(add(1, 2)), 100);
}
export async function lazy() {
const m = await import('./heavy.js');
return m.heavy();
}
// src/heavy.js
export const heavy = () => 'heavy';
次の設定は、ESM版をチャンク分割したままdist/esmへ、CommonJS版を1ファイルに畳み込んでdist/index.cjsへ出力します。
// rollup.config.mjs
import { nodeResolve } from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import terser from '@rollup/plugin-terser';
export default {
input: 'src/main.js',
external: ['lodash-es'],
output: [
{ dir: 'dist/esm', format: 'es' },
{ file: 'dist/index.cjs', format: 'cjs', inlineDynamicImports: true },
],
plugins: [nodeResolve(), commonjs(), terser()],
};
実行するとcreated dist/esm, dist/index.cjs in 280msと出て、dist/esm/main.js(185バイト)、動的importで切り出された dist/esm/heavy-Cq6uRrcA.js(40バイト)、dist/index.cjs(263バイト)の3ファイルができました。
ESM側を dir でなく file で書くと、Invalid value for option "output.file" - when building multiple chunks, the "output.dir" option must be used, not "output.file".で止まります。動的importがあると出力が複数チャンクになるためです。単一のinputを1ファイルにまとめたい出力には inlineDynamicImports: true を付けます。ただし、動的import先のモジュール本体が直ちに実行されるため、副作用の実行順序が変わり得る点には注意が必要です。チャンクを分割したい出力は dir にします。
生成したESM版の.jsをNode.jsから直接読み込む場合は、配布するpackage.jsonを "type": "module" にします。初期状態の "type": "commonjs" のままでは、dist/esm内の.jsもCommonJSとして扱われます。npm公開時はexportsでESM版とCommonJS版の入口も指定してください。
rollup.config.jsがCommonJSとして読まれるエラーの原因と直し方
設定ファイルを rollup.config.js という名前でimport構文で書くと、次のエラーで止まることがあります。
[!] RollupError: Node tried to load your configuration file as CommonJS even though it is likely an ES module. To resolve this, change the extension of your configuration to ".mjs", set "type": "module" in your package.json file or pass the "--bundleConfigAsCjs" flag.
Original error: Cannot use import statement outside a module
Rollupは設定ファイルをNode.jsにそのままimportさせるので、.jsファイルをESMとして扱うかどうかはpackage.jsonの "type" で決まります。npm 11.17.0で npm init -y を実行すると "type": "commonjs" が書き込まれたため、初期状態の.jsはCommonJS扱いです。直し方はメッセージのとおり3つあります。
- 設定ファイルを
rollup.config.mjsに改名する(他のファイルに影響しないので最も安全) - package.jsonを
"type": "module"にする(プロジェクト内の.jsがすべてESM扱いになる) rollup -c --bundleConfigAsCjsで実行する(Rollupが設定ファイルをCommonJSへ変換してから読む)
rollup.config.tsをTypeScriptで書く2つの方法
公式の方法は、@rollup/plugin-typescript を入れて --configPlugin を付けることです。
npm i -D @rollup/plugin-typescript typescript@6 tslib
npx rollup -c rollup.config.ts --configPlugin typescript
ここで typescript@6 と版を指定しているのには理由があります。npmのlatestは2026年7月8日公開のTypeScript 7.0.2で、版を付けずに入れると7が入ります。この組み合わせでは @rollup/plugin-typescript 12.3.0が読み込み時にCannot read properties of undefined (reading 'ES2015')で落ち、--configPlugin はCannot find entry for plugin "typescript"を返しました。TypeScript 7.0.2のtypescriptパッケージは、既定のエクスポートが version と versionMajorMinor の2つだけで、プラグインが参照する ModuleKind などのコンパイラーAPIがありません。TypeScript 7への対応を求めるrollup/pluginsのissue #2016(2026年7月10日起票)は、9月27日時点でopenのままです。6.0.3に固定すると設定ファイルを読み込めましたが、TS5107: Option 'moduleResolution=node10' is deprecatedの警告が出ます。
もう1つの方法はプラグインを使わないやり方です。型注釈を取り除く機能が既定で有効なNode v26.5.0では、package.jsonを "type": "module" にすると、--configPlugin なしの npx rollup -c rollup.config.ts がそのまま通りました("type": "commonjs" のままだと、上のCommonJSのエラーになります)。ただし、この方法では型チェックは行われず、tsconfig.jsonも参照されません。型検査はTypeScriptで別途実行してください。
// rollup.config.ts
import type { RollupOptions } from 'rollup';
import { nodeResolve } from '@rollup/plugin-node-resolve';
const config: RollupOptions = {
input: 'src/main.js',
output: { dir: 'dist', format: 'es' },
plugins: [nodeResolve()],
};
export default config;
node-resolve・commonjsなど主要プラグインの役割と導入時の注意点
| プラグイン | 最新版 | 役割 |
|---|---|---|
| node-resolve(正式名は本文) | 16.0.3 | node_modulesのパッケージを探す |
| @rollup/plugin-commonjs | 29.0.3 | CommonJSをESMへ変換 |
| @rollup/plugin-babel | 7.1.0 | Babelで構文を変換 |
| @rollup/plugin-typescript | 12.3.0 | TypeScriptをコンパイル |
| @rollup/plugin-terser | 1.0.0 | 出力を圧縮 |
| @rollup/plugin-json | 6.1.0 | JSONをimport可能にする |
版は2026年9月27日にnpm registryで確認した値です。6つともpeerDependenciesにRollup 4を含んでいます。ただし、@rollup/plugin-terser 1.0.0のNode.js要件は20以上で、Rollup本体の18以上とは異なります。
node-resolveとcommonjsによる依存パッケージ取り込み設定
表のnode-resolveの正式なパッケージ名は @rollup/plugin-node-resolve です。
Rollup本体は import 'lodash-es' のようなパッケージ名を解決しません。プラグインなしでバンドルすると(!) Unresolved dependenciesと警告し、そのimportを外部依存として出力に残します。バンドルに含めたいなら @rollup/plugin-node-resolve を足します。前述のとおり、package.jsonの sideEffects を読んでツリーシェイキングに反映するのもこのプラグインです。
読み込むパッケージがCommonJS製(module.exportsで書かれている)の場合は、さらに @rollup/plugin-commonjs が要ります。CommonJS版のlodashを import _ from 'lodash' で読み、node-resolveだけでビルドすると"default" is not exported by "node_modules/lodash/lodash.js"で止まりました。commonjsプラグインを足すと通ります。
1つのファイルにimportとrequireが混在している場合、commonjsプラグインは既定では require を変換しません。オプション transformMixedEsModules の既定値はfalseで、実際にimportと require('path') が混在したファイルをcjs形式で出力すると、const path = require('path'); がそのまま残りました。trueにすると var require$$0 = require('path'); のようにRollupの管理下へ移ります。ブラウザ向けにES形式で出す場合は、生の require を残さないようtrueにします。ただし、例のpathはNode.js組み込みモジュールなので、これだけではブラウザで動きません。ブラウザ向けの代替実装への置換なども必要です。公式READMEは、環境判定のコードや、Electronのように require を特別扱いする環境ではfalseのまま残すべきだとしています。
commonjs({ transformMixedEsModules: true })
Babel 8と@rollup/plugin-babelのpeer依存の衝突
古い構文への変換にBabelを使う場合、プラグインとBabelをまとめて入れると2026年9月時点ではインストールに失敗します。
npm i -D @rollup/plugin-babel @babel/core @babel/preset-env
# npm error code ERESOLVE(Conflicting peer dependency で停止)
エラー出力には、衝突相手として@babel/coreの7系(7.29.7)と、それを要求する@rollup/plugin-babel 7.1.0が表示されます。@babel/coreのlatestが8.0.6になった一方、@rollup/plugin-babel 7.1.0のpeerDependenciesは "@babel/core": "^7.0.0" のままだからです。@babel/preset-envのlatestも8系で、こちらは @babel/core ^8.0.0 を要求するので両立しません。Babel側を7系に揃えると入ります。
npm i -D @rollup/plugin-babel @babel/core@7 @babel/preset-env@7
--legacy-peer-deps で警告を無視して入れる方法もありますが、プラグインが想定していないBabel 8のAPIで動かすことになるので勧めません。
lodashの読み込み方で比べるツリーシェイキングの効果:非圧縮で約44倍の差
ESMは静的解析しやすい形式ですが、削減量は副作用の有無、exportの使い方、パッケージの実装にも左右されます。ここではlodashの読み込み方による差を比較します。debounceを1つだけ使う入口ファイルを3通りの書き方で用意し、node-resolveとcommonjsを通してes形式で出力しました(lodash・lodash-esとも4.18.1)。入口ファイルと計測コマンドは次のとおりです。
// a.js
import { debounce } from 'lodash-es';
export const f = debounce(() => 1, 10);
// b.js
import _ from 'lodash';
export const f = _.debounce(() => 1, 10);
// c.js
import debounce from 'lodash/debounce.js';
export const f = debounce(() => 1, 10);
npx rollup a.js -f es -o a.out.js -p node-resolve -p commonjs
npx rollup a.js -f es -o a.min.js -p node-resolve -p commonjs -p terser
wc -c a.out.js a.min.js
gzip -c a.min.js | wc -c # macOS標準のgzipで計測
| 読み込み方 | 圧縮なし | terser後 | terser+gzip |
|---|---|---|---|
| lodash-esの名前付きimport | 13,258 | 2,189 | 1,037 |
| lodashの関数パス指定 | 17,446 | 3,110 | 1,351 |
| import _ from ‘lodash’ | 578,775 | 71,426 | 25,344 |
表の「lodash-esの名前付きimport」は import { debounce } from 'lodash-es'、「lodashの関数パス指定」は import debounce from 'lodash/debounce.js' に対応し、それぞれ上のa.jsとc.jsの読み込み方です。単位はバイトです。CommonJS版を丸ごとimportすると、使っていない関数ごとlodash全体が出力に入り、ESM版の約44倍になりました。CommonJS版でもファイル単位のパス(lodash/debounce.js)を指定すれば、読み込むファイル自体が減るので差は小さくなります。依存パッケージを選ぶときは、package.jsonの exports や module にESM版があるかを先に確認してください。CommonJSしか無いパッケージは、関数単位のパスからimportできるかを調べます。
externalで依存パッケージをバンドルに含めないnpmライブラリ向けの設定
npmライブラリを作るときは、dependenciesやpeerDependenciesに書いたパッケージをバンドルに含めず、利用者側のnode_modulesから読ませるのが基本です。含めてしまうと、利用者のアプリに同じライブラリが2重に入ります。external に渡せるのは配列・正規表現・関数で、配列に指定する名前はimport文の表記どおりです。lodash-es/debounce のようなサブパスまで外すには、関数で前方一致させます。
// rollup.config.mjs
import pkg from './package.json' with { type: 'json' };
const deps = [
...Object.keys(pkg.dependencies ?? {}),
...Object.keys(pkg.peerDependencies ?? {}),
];
export default {
input: 'src/main.js',
external: (id) => deps.some((dep) => id === dep || id.startsWith(`${dep}/`)),
output: { dir: 'dist', format: 'es' },
};
dependenciesにlodash-esを書いた状態でビルドすると、出力の先頭行は import { debounce } from 'lodash-es'; のまま残り、lodash-es本体はバンドルに入りませんでした。外部依存のツリーシェイキングは、ライブラリの利用者側のバンドラーが行います。だからこそ、ESM形式で配り、package.jsonの sideEffects を正しく書いておくことが、利用者のバンドルサイズに直結します。scriptタグで読ませるiife形式でも、依存ライブラリを先に読み込み、output.globalsで外部依存とグローバル変数名を対応付ければexternalの使用が可能です。依存を含めた単独ファイルとして配る場合は、node-resolveで取り込みます。
Webpack・Vite・Rolldown・esbuildとの違いと選び方
Vite 8で本番ビルドがRollupからRolldownへ移った経緯
Vite 7までは、開発時の変換にesbuild、本番ビルドにRollupという2つのバンドラーを使い分けていました。2026年3月12日に公開されたVite 8は、これをRust製のRolldownに一本化したバージョンです。Viteチームの発表は、Rolldownを「10-30x faster than Rollup」とし、RollupやViteと同じプラグインAPIを持つため既存のViteプラグインの多くがそのまま動くと説明しています。Viteでのプロジェクト作成手順はViteの環境構築とVite 8の変更点にまとめています。
npm registryで確認すると、vite 8.3.1のdependenciesには rolldown ~1.2.9 があり、rollupは含まれていません。設定項目も、Vite公式ドキュメントでは build.rollupOptions がDeprecated扱いの別名になり、build.rolldownOptions への移行が案内されています。Rolldown自体は2026年5月7日に1.0.0となり、9月27日時点の最新は1.2.11です。
つまり「ViteはRollupでビルドしている」という説明はVite 7までの話です。Vite 8のプロジェクトでビルドの挙動を調べるときは、RollupではなくRolldownのドキュメントを見ます。RolldownのREADMEは「Rollup-compatible APIs and plugin interface」を持つとしつつ、スコープは「more similar to esbuild」だと書いています。Rollupの設定をRolldownへそのまま移せるとは限りません。
webpackの削除判定:usedExportsとsideEffectsの役割
Rollupのツリーシェイキングは既定で有効で、モードの切り替えは要りません。webpackは mode: 'production' で最小化とツリーシェイキングが有効になる設計で、ガイドによると、ツリーシェイキングに必要な ModuleConcatenationPlugin もproductionモードで追加されます。
webpackの仕組みは2段構えです。usedExports の役割は未使用のexportに印を付けることで、文単位の副作用判定を担うのはterserです。sideEffects はモジュールやファイルのサブツリーごと読み飛ばせるため、ガイドは「much more effective」としています。package.jsonの sideEffects と #__PURE__ 注釈は両者で共通に使えるので、ライブラリ作者が正しく書いておけば、利用者がRollupでもwebpackでも効きます。#__NO_SIDE_EFFECTS__ はwebpack側では5.107.0以降の対応です。Next.jsの既定バンドラーになったTurbopackの設定と移行判断は別記事で扱っています。
ライブラリ・SPA・CLIスクリプト別に見るバンドラー選定の判断基準
| 作るもの | 第一候補 | 理由 |
|---|---|---|
| npmライブラリ(ESM+CJS) | Rollup | 出力形式とチャンクを細かく制御できる |
| SPA・Webアプリ | Vite | 開発サーバーとHMRが揃う |
| 既存のwebpack構成 | webpack継続 | ローダー資産の移行コストが大きい |
| CLI・スクリプトの単発ビルド | esbuild | 設定がほぼ不要で速い |
新規にWebアプリを作るなら、Rollupを直接選ぶ理由はほぼありません。Viteを使えば開発サーバー、HMR、本番ビルドが一式で揃い、Rollup形式のプラグインもおおむねそのまま使えます。Rollupを直接使うのは、npmへ公開するライブラリの出力(ESMとCJSの両方、型定義との対応、externalの細かい指定)を自分で制御したい場合と、Viteを使わないビルド処理です。CLIツールの単発ビルドで速さを優先するなら、Go製バンドラーesbuildの使い方と採用判断も比較対象になります。Rollupのプラグインに依存していてRolldownでの動作が未確認なら、Rollupのまま使い続けるのが安全です。
Rollupでよく出るエラー4種:ネイティブバイナリ欠落やthis書き換え警告の対処
ここまでの章で扱っていないエラーをまとめます。
- Cannot find module @rollup/rollup-(OS名):ネイティブバイナリが入っていません。メッセージはnpmのoptionalDependenciesのバグ(npm/cli issue #4828)に触れ、package-lock.jsonとnode_modulesの両方を消してから
npm iをやり直すよう案内します。別OSで作ったlockfileを持ち込んだときに起きます。ネイティブバイナリが用意されていない環境では、同じインターフェースの@rollup/wasm-nodeに置き換えます。 - Failed to load module … Required DLL was not found:Windowsで、Microsoft Visual C++ 再頒布可能パッケージが入っていないときに出ます。メッセージに表示されるダウンロードリンクから入れます。
"this" has been rewritten to "undefined":トップレベルでthisを参照するファイルをESMとして読んだときに出ます。典型はCommonJSやUMDのファイルを変換せずに読んだ場合です。ESMのトップレベルのthisはundefinedだからです。CommonJS由来ならcommonjsプラグインによる変換を確認します。ただし、ESM自体がトップレベルのthisを参照している場合にも同じ警告が出るため、参照元を修正する必要があります。グローバルオブジェクトを必要とする既存コードでは、意図を確認したうえでcontextまたはmoduleContextを検討します。- ウォッチモードがCtrl+Cで終わらない:4.63.5の変更履歴に「Fix an issue where watch mode would hang instead of terminating when closing via Ctrl+C」とあります。
rollup -wが止まらない場合は、まず4.63.5以上へ上げてください。
ツリーシェイキング前提のライブラリ設計で採用する条件と見送る場面
ツリーシェイキングの効果を最も大きく左右するのは、バンドラーの設定ではなく、配布するパッケージの作り方です。実務では、次の順で確認すれば足ります。
- ESM版を配布しているか(package.jsonの
exportsにimport条件があるか) - モジュールのトップレベルが宣言とexportだけになっているか
- その状態を
sideEffectsで宣言し、CSSとpolyfillを配列で除外しているか - 公開しているファクトリ関数に
#__NO_SIDE_EFFECTS__を付けているか
"sideEffects": false を書いてよいのは、2番目が守られている場合だけです。トップレベルで customElements.define() を呼んでWeb Componentsを登録する、Array.prototype を拡張する、グローバル変数にロガーを差し込む、といった処理を持つパッケージには書きません。利用者がimportしただけで登録されることを期待しているのに、本番ビルドだけ登録が消えるという、開発環境では再現しない不具合になります。
treeshake.preset: 'smallest' は、自社で書いたコードだけをまとめ、依存にpolyfillや機能検出のコードを含まない場合に限って採用します。サードパーティの依存を多く抱えるアプリでは見送り、recommended にとどめてください。数KBの削減と引き換えに、try文の中の機能検出が消えるリスクを負う価値はありません。
逆に、ツリーシェイキングに頼りすぎない方がよいのは、依存がCommonJSのみの場合です。CommonJSしか配布していない依存を1〜2関数だけ使っているなら、バンドラーの設定を詰めるより、関数単位のパスからimportするか、ESM版のある別パッケージへ置き換えた方が確実に小さくなります。社内向けの業務システムで、ビルド構成やnpmライブラリの切り出しから見直したい場合は、一創のフルスクラッチ開発でフロントエンドの構成設計から相談を受けています。
よくある質問
Rollupの導入とツリーシェイキングについて、よく検索されている疑問に答えます。
Rollupとnpmは何が違いますか?
npmはパッケージを入れたり公開したりするパッケージマネージャーで、Rollupはソースコードをまとめるバンドラーです。役割が違うため比較の対象にはならず、npmでRollupを入れ、npm scriptsからRollupを実行する、という組み合わせで使います。
Rollupはグローバルにインストールすべきですか?
プロジェクトごとに npm i -D rollup で入れるのが基本です。グローバルに入れると、プロジェクトごとに違う版を使えず、CIの版とも揃いません。単発で試すだけなら、事前にプロジェクトへ追加せず npx rollup で実行できます。ローカルにない場合は、npmが必要なパッケージをキャッシュ領域へインストールします。
ツリーシェイキングが効いているかはどう確かめればよいですか?
同じ入口ファイルを、通常のビルドと --no-treeshake を付けたビルドの2通りで出力し、wc -c でバイト数を比べます。差がほとんど無ければ、依存がCommonJSである、副作用ありと判定されたモジュールがある、のどちらかを疑います。特定の関数が残っていないかを確かめる方法は、出力を関数名で grep することです。残っていた場合は、その関数を呼んでいるトップレベルの式に #__PURE__ 注釈を付けられるかを確認します。
inlineDynamicImportsはどこに書けばよいですか?
Rollup 4では output の中に書きます。4.0.0で入力側のオプションから削除されたため、3.x以前のように設定のトップレベルに書くと効きません。CLIでは --inlineDynamicImports です。既定値はfalseです。
Viteを使っていればRollupを別にインストールする必要はありますか?
Vite 8以降では不要です。Vite 8はRolldownを依存に持ち、Rollupは入りません。Vite 7のdependenciesには rollup ^4.43.0 が含まれていたので、Vite 7以前でもRollupを別に入れる必要はありませんでした。