Tree Shaking — アプリケーションのビルド段階で未使用コードを削除するメカニズム(dead code elimination)です。Tree ShakingはESモジュールの静的構造を分析し、どこにもインポートされていないエクスポートされた関数、クラス、変数を除外します。Webpack Documentationによると、Tree Shakingを適切に設定すると、アプリケーションの機能を変更せずにバンドルサイズを30~60%削減できます。
重要なポイント
Tree Shaking — 最終バンドルから未使用のモジュールや関数を除外するコード最適化手法です。この用語は2015年にRollupチームによって導入され、プロセスを比喩的に表現しています。依存関係のツリーを振る(shake)と、使われていない枝が落ちるというイメージです。手動最適化とは異なり、Tree Shakingはビルド段階で自動的に実行されます。
Tree ShakingはESモジュール(ECMAScript Modules)でのみ動作します。ESモジュールでは依存関係がimportとexportを通じて静的に決定されます。CommonJS(require/module.exports)はTree Shakingをサポートしません。requireが動的に実行されるため、バンドラーはどの関数が実際に使用されるかを事前に判断できないからです。最新のライブラリ(Lodash、Moment.js、RxJS)はTree ShakingをサポートするためにESバージョンを提供しています。
Rollup — 2015年にTree Shakingを最初に実装したバンドラーです。Webpackとは異なり、Rollupは最初からESモジュール用に設計されており、より積極的にデッドコードを削除します。Rollupは個々のエクスポートだけでなくモジュール全体を分析します。モジュールに副作用がなく、どのエクスポートも使用されていない場合、Rollupはモジュール全体をバンドルから除外します。
Rollupは、1キロバイトが重要となるライブラリやSDKに特に効果的です。Vue.jsフレームワークはプロダクションバージョンのビルドにRollupを使用しています。Reactは2020年にRollupに移行しました。アプリケーションでは、より豊富なプラグインエコシステム(Hot Module Replacement、code splitting、CSS modules)のためWebpackがより一般的に使用されていますが、ライブラリをビルドする際の最大のTree Shaking効果を得るには、Rollupが業界標準であり続けています。
Tree Shakingによる削減効果はプロジェクトのアーキテクチャに大きく依存します。Ant Designライブラリを使用したReactアプリケーションでは、Tree ShakingはUIコンポーネントのコードを最大70%削減できます。すべてのインポートが具体的で限定的なプロジェクトでは、削減率は5~15%になります。Webpackの調査によると、平均削減率はバンドルサイズの30~40%です。
| デッドコードの種類 | 例 | Tree Shakingの検出 |
|---|---|---|
| 未使用のエクスポート | export function unusedHelper() | はい |
| 未使用のインポート | import { unused } from "lib" | はい |
| デッド条件分岐 | if (false) { ... } | いいえ(ミニファイアが削除) |
| DCE後の未呼び出し関数 | function a(){} a() でaが呼び出されていない | 部分的 |
Tree Shakingのメカニズムは、バンドラーがプロジェクト内のすべてのimport/export文から構築する依存関係グラフに基づいています。最初の段階で、バンドラーはエントリポイントからすべてのファイルをトラバースし、モジュールツリーを収集します。次の段階で、各モジュールのどのエクスポートが他のモジュールで実際にインポートされているかを分析します。
各モジュールについて、WebpackまたはRollupはエクスポートを使用済みまたは未使用としてマークします。未使用のエクスポートはバンドルから除外されます。ただし、モジュール自体は、そのエクスポートの少なくとも1つが使用されている場合、バンドルに残ります。モジュールを完全に除外できるのは、sideEffectsフラグを使用するか、モジュールに副作用がまったくない場合のみです。
// utils.js — 関数を含むモジュール
export function formatDate(date) {
return date.toISOString().slice(0, 10);
}
export function formatCurrency(amount) {
return "$" + amount.toFixed(2);
}
export function slugify(text) {
return text.toLowerCase().replace(/\s+/g, "-");
}// app.js — エントリポイント
import { formatDate } from "./utils";
const today = formatDate(new Date());
console.log(today);// Tree Shaking後 — バンドル内はformatDateのみ
function formatDate(date) {
return date.toISOString().slice(0, 10);
}
const today = formatDate(new Date());
console.log(today);Tree Shakingにより、formatCurrencyとslugifyはapp.jsでインポートされていないため、最終バンドルから除外されました。utils.jsモジュールのサイズは3関数から1関数に減少しました。utils.jsに副作用(グローバル初期化など)がある場合、Tree Shakingは未使用のエクスポートも削除できません。
Webpackは、プロダクションモードでTerserPluginを通じて組み込みのTree Shakingサポートを提供します。Tree Shakingを有効にするには、2つの条件で十分です。modeがproduction(mode: "production")に設定され、モジュールがES構文(import/export)を使用していること。Webpackは自動的に未使用のエクスポートをマークし、Terserに渡して削除します。
optimization.webpack.config.jsでのusedExports: trueの追加設定により、モジュール内のエクスポート使用状況の詳細な分析が可能になります。このオプションは、どのエクスポートが実際に使用され、どのエクスポートが単にエクスポートされているだけか(provided)を判断します。usedExportsとTerserの組み合わせにより、最大のデッドコード削除効率が得られます。
// webpack.config.js — Tree Shaking設定
module.exports = {
mode: "production",
entry: "./src/app.js",
output: {
filename: "bundle.js",
},
optimization: {
usedExports: true,
minimize: true,
concatenateModules: true,
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules\/(?!(my-lib)\/).*/,
use: {
loader: "babel-loader",
options: {
presets: [
["@babel/preset-env", { modules: false }],
],
},
},
},
],
},
};重要なパラメータは、@babel/preset-envのmodules: falseです。デフォルトでは、BabelはESモジュールをCommonJSに変換し、Tree Shakingを無効にします。modules: falseはBabelがimport/exportを変換するのを防ぎ、Webpack用にES構文を保持します。concatenateModulesはさらにモジュールを共有スコープにマージし、IIFEの数を減らしてバンドルサイズを削減します。
Side effects(副作用)とは、モジュールがインポートされたときに実行される、エクスポートされた値に関係のないアクションです。グローバルスタイル(import "./styles.css")、ポリフィル(import "core-js/stable")、グローバル変数の初期化、Service Workerの登録などです。モジュールに副作用が含まれている場合、バンドラーはそのエクスポートが1つも使用されていなくても、モジュールをバンドルから安全に削除できません。
package.jsonのsideEffectsフラグは、パッケージ内のどのモジュールに副作用がないかをバンドラーに通知します。すべてのモジュールが純粋(関数のみをエクスポート)なパッケージの場合は、"sideEffects": falseを指定します。CSSやポリフィルを含むパッケージの場合は、副作用のあるファイルへのパスの配列を指定します:"sideEffects": ["*.css"]。このフラグがないと、Tree Shakingは未使用の関数も削除しません。
モジュールに副作用があるかどうかを確認するには、次の質問をしてください。このインポートは、値のエクスポートに関係のないアクションを実行しますか? import "./styles.css"はCSSをDOMに追加します — これは副作用です。import { throttle } from "lodash-es"には副作用がありません — throttle関数を利用可能にするだけです。ポリフィル(import "core-js/stable")には副作用があります — グローバルプロトタイプを変更します。
独自のモジュールについては、以下を推奨します。スタイルとポリフィルを別のエントリポイントに分離し、純粋なユーティリティ(副作用のない関数)と副作用のあるモジュール(初期化、ロギング、Service Worker登録)を分離します。トップレベルのプロジェクトpackage.jsonでは、すべてのモジュールが純粋な場合にのみ"sideEffects": falseを指定します。スタイルがある場合は、"sideEffects": ["*.css"]を正確に指定します。
{
"name": "my-ui-lib",
"version": "2.1.0",
"sideEffects": [
"*.css",
"polyfills.js"
],
"module": "dist/index.esm.js",
"main": "dist/index.cjs.js"
}"sideEffects": ["*.css", "polyfills.js"]は、すべてのCSSファイルに副作用があり(削除できない)、polyfills.jsにも副作用があることを意味します。パッケージ内の他のすべてのJSファイルは純粋で、安全にshakeできます。moduleフィールドは、Tree ShakingのためにバンドラーがCommonJSバージョン(main)の代わりに使用するESバージョンのパッケージへのパスを指定します。
React NativeのMetro Bundlerは、限定されたバージョンのTree Shakingをサポートしています。MetroはWebpackのようにusedExportsの完全な静的解析を実行しません。代わりに、Metroはミニフィケーション中にTerserに依存してモジュールの未使用部分を削除します。このアプローチの効果は、Webpackでの完全なTree Shakingよりも低くなります。
React Nativeプロジェクトを最大限に最適化するには、以下を推奨します。ESモジュールを使用したライブラリを使用し(package.jsonのmoduleフィールド)、デバッグコードを削除するためにbabel-plugin-transform-remove-consoleを追加し、Terser用にMetro transformer.minifierConfigを設定します。さらに、Ram Bundle(バンドルをモジュールに分割)により、未使用の画面の読み込みが減少します。
よくある質問
CommonJS(require/module.exports)は静的解析をサポートしていません — requireは条件や関数内で動的に呼び出される可能性があります。バンドラーはモジュールのどの部分が実際に使用されているかを判断できません。静的なimport/exportを持つESモジュールのみがTree Shakingを可能にします。
TypeScriptは、tsconfig.jsonがESモジュール用に設定されている場合("module": "esnext")、Tree Shakingと完全に互換性があります。TypeScriptコンパイラは、import/exportをCommonJSに変換せずに保持する必要があります。@babel/preset-typescriptとmodules: falseを指定したBabelも、ESモジュールをWebpackに正しく渡します。
Webpack Bundle Analyzer — バンドルの構成をインタラクティブなダイアグラムとして視覚化するプラグインです。ライブラリがバンドルに存在するがその関数が使用されていない場合、Tree Shakingは機能していません。出力ファイルを分析することもできます。grepを使用してバンドルテキスト内の未使用のエクスポートを検索します。
Lodash v4はCommonJSパッケージとして配布されています。Tree Shakingを行うには、ライブラリのESバージョンであるlodash-esを使用する必要があります。import throttle from "lodash/throttle"をimport { throttle } from "lodash-es"に置き換え、Webpackでresolve.aliasを設定してlodashをlodash-esに置き換えます。
Tree Shakingは、依存関係グラフの分析と使用済みエクスポートのマーク付けの段階を追加するため、ビルド時間がわずかに増加します(5~15%)。開発モードでは、Tree Shakingは通常、速度のために無効になっています。本番環境では、追加の時間はバンドルサイズの大幅な削減によって正当化されます。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。