Lodash のツリーシェイクが効かない問題を解決:import 形態とバンドラ設定
JavaScript プロジェクトで Lodash を使っていると、バンドルサイズが予想以上に大きくなってしまうことがありますよね。実は、Lodash の import の仕方やバンドラの設定によって、使っていない関数まで含まれてしまうことが原因なんです。
この記事では、Lodash のツリーシェイクが効かない理由と、具体的な解決策をご紹介します。正しい import 形態とバンドラ設定を理解すれば、バンドルサイズを劇的に削減できますよ。
背景
Lodash とは
Lodash は、JavaScript で配列やオブジェクトの操作を簡単に行える便利なユーティリティライブラリです。map、filter、debounce など、200 以上もの関数が用意されており、多くのプロジェクトで採用されていますね。
しかし、Lodash のフルビルド版は 約 70KB(圧縮後で 24KB) もあり、実際には数個の関数しか使わないのに、すべての関数がバンドルに含まれてしまうケースが少なくありません。
ツリーシェイクとは
ツリーシェイク(Tree Shaking)は、使われていないコードを自動的に削除する最適化技術でしょう。webpack や Rollup などのモダンバンドラは、ES Modules(import/export)の静的解析により、実際に使われている関数だけをバンドルに含めます。
下図は、ツリーシェイクの基本的な動作を示しています。
mermaidflowchart LR
src["ソースコード<br/>(全関数)"] -->|バンドル時| analyze["静的解析"]
analyze -->|使用中| keep["使用関数のみ<br/>バンドルへ"]
analyze -->|未使用| remove["未使用関数<br/>削除"]
keep --> bundle["最終バンドル"]
要点: ツリーシェイクは、import の形式がモジュール単位で静的に解析できる場合にのみ機能します。
Lodash でツリーシェイクが効かない理由
Lodash の標準パッケージ(lodash)は、CommonJS 形式でビルドされているため、多くのバンドラではツリーシェイクが効きません。また、以下のような import 形式を使うと、Lodash 全体がバンドルに含まれてしまいます。
typescript// ❌ ツリーシェイクが効かない例
import _ from 'lodash';
import { map, filter } from 'lodash';
const result = _.map([1, 2, 3], (n) => n * 2);
この場合、map や filter だけを使いたいのに、Lodash のすべての関数がバンドルされてしまうんです。
課題
バンドルサイズの肥大化
Lodash 全体をバンドルに含めてしまうと、以下のような問題が発生します。
| # | 課題 | 影響 |
|---|---|---|
| 1 | バンドルサイズの増加 | 初回ロード時間が長くなり、ユーザー体験が低下する |
| 2 | ネットワーク転送量の増加 | モバイル環境などで通信コストが増える |
| 3 | パース・実行時間の増加 | JavaScript の解析と実行に時間がかかり、表示が遅れる |
| 4 | キャッシュ効率の低下 | 不要なコードが含まれるため、キャッシュが非効率になる |
実際のバンドルサイズ比較
以下の表は、異なる import 形式でのバンドルサイズの違いを示しています。
| # | import 形式 | バンドルサイズ(圧縮前) | バンドルサイズ(gzip 後) |
|---|---|---|---|
| 1 | import _ from 'lodash' | 70KB | 24KB |
| 2 | import { map } from 'lodash' | 70KB | 24KB |
| 3 | import map from 'lodash/map' | 6KB | 2KB |
| 4 | import { map } from 'lodash-es' | 2KB | 0.7KB |
この表を見ると、適切な import 形式を選ぶだけで、バンドルサイズを 10 分の 1 以下 に削減できることがわかりますね。
よくある誤解
開発者が陥りやすい誤解をまとめました。
- 誤解 1: 名前付き import(
{ map })を使えば自動的にツリーシェイクされる- 実態: Lodash の標準パッケージは CommonJS なので効きません
- 誤解 2: webpack を使えば自動的に最適化される
- 実態: バンドラだけでは不十分で、パッケージ側が ES Modules に対応している必要があります
- 誤解 3:
lodash-esに変えるだけで OK- 実態: バンドラ側でサイドエフェクトフリー設定(
sideEffects: false)も必要です
- 実態: バンドラ側でサイドエフェクトフリー設定(
解決策
Lodash のツリーシェイクを有効にするには、以下の 3 つのアプローチがあります。それぞれにメリット・デメリットがあるため、プロジェクトの状況に応じて選択しましょう。
解決策 1:個別 import を使う
最もシンプルな方法は、関数ごとに個別にインポートすることです。
typescript// ✅ 個別 import でツリーシェイクが不要に
import map from 'lodash/map';
import filter from 'lodash/filter';
import debounce from 'lodash/debounce';
const result = map([1, 2, 3], (n) => n * 2);
const filtered = filter(result, (n) => n > 2);
メリット:
- バンドラの設定変更が不要
- 確実に必要な関数だけがバンドルされる
- TypeScript の型定義も正常に動作
デメリット:
- import 文が増えて煩雑になる
- チーム内での徹底が必要
解決策 2:lodash-es を使う
Lodash の ES Modules 版である lodash-es を使うと、モダンバンドラでツリーシェイクが効くようになります。
以下は、lodash から lodash-es への移行手順です。
ステップ 1:パッケージのインストール
まず、既存の lodash を削除して lodash-es をインストールしましょう。
bash# lodash を削除
yarn remove lodash
# lodash-es をインストール
yarn add lodash-es
yarn add -D @types/lodash-es
ステップ 2:import 文の変更
既存のコードの import 文を書き換えます。
typescript// ❌ 変更前
import _ from 'lodash';
import { map, filter } from 'lodash';
// ✅ 変更後
import { map, filter } from 'lodash-es';
const result = map([1, 2, 3], (n) => n * 2);
const filtered = filter(result, (n) => n > 2);
これで、map と filter だけがバンドルに含まれるようになります。デフォルト import(import _)を使っている箇所は、名前付き import に変更する必要があります。
ステップ 3:バンドラ設定の確認
webpack を使っている場合、package.json に sideEffects の設定を確認しましょう。
json{
"sideEffects": false
}
この設定により、webpack は未使用の export を削除できるようになります。lodash-es 自体は sideEffects: false が設定されているため、追加の設定は不要です。
メリット:
- 名前付き import が使えて、コードが読みやすい
- TypeScript の型推論が正確に働く
- モダンバンドラ(webpack 4+、Rollup、Vite など)で自動的に最適化される
デメリット:
- 既存コードの書き換えが必要
- 古いバンドラでは対応していない場合がある
解決策 3:babel-plugin-lodash を使う
既存コードを変更せずに最適化したい場合は、babel-plugin-lodash を使う方法があります。
ステップ 1:プラグインのインストール
bashyarn add -D babel-plugin-lodash
ステップ 2:Babel 設定の追加
.babelrc または babel.config.js にプラグインを追加します。
json{
"plugins": ["lodash"]
}
ステップ 3:動作確認
このプラグインは、ビルド時に自動的に import 文を最適化します。
typescript// 🔄 変換前(記述するコード)
import { map, filter } from 'lodash';
const result = map([1, 2, 3], (n) => n * 2);
const filtered = filter(result, (n) => n > 2);
typescript// ✅ 変換後(ビルド時の実際のコード)
import map from 'lodash/map';
import filter from 'lodash/filter';
const result = map([1, 2, 3], (n) => n * 2);
const filtered = filter(result, (n) => n > 2);
メリット:
- 既存コードの変更が最小限
- 開発者は通常の import 文を書けばよい
- 自動的に最適化される
デメリット:
- Babel の設定が必要
- ビルドステップが増える
- TypeScript の
tscだけを使っている場合は利用できない
推奨する選択基準
プロジェクトの状況に応じて、以下の基準で選択するとよいでしょう。
| # | 状況 | 推奨する解決策 |
|---|---|---|
| 1 | 新規プロジェクト | lodash-es を採用 |
| 2 | 既存プロジェクトで書き換えが可能 | lodash-es へ段階的に移行 |
| 3 | 既存コードを変更したくない | babel-plugin-lodash を導入 |
| 4 | Lodash の使用箇所が少ない | 個別 import に書き換え |
| 5 | バンドラを使っていない(Node.js など) | 個別 import を使う |
具体例
実際のプロジェクトで、どのようにツリーシェイクを適用するかを見ていきましょう。Next.js プロジェクトを例に、段階的に最適化を進めます。
プロジェクトの初期状態
以下は、典型的な Next.js プロジェクトで Lodash を使っている例です。
typescript// pages/index.tsx
import _ from 'lodash';
import { useState } from 'react';
export default function Home() {
const [items, setItems] = useState([1, 2, 3, 4, 5]);
// Lodash の map と filter を使用
const processedItems = _.chain(items)
.map((n) => n * 2)
.filter((n) => n > 5)
.value();
return (
<div>
<h1>処理結果</h1>
<ul>
{processedItems.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
</div>
);
}
この状態で Next.js の本番ビルドを実行すると、バンドルサイズは以下のようになります。
bashyarn build
textPage Size First Load JS
┌ ○ / 2.5 kB 95.2 kB
└ ○ /404 194 B 92.9 kB
+ First Load JS shared by all 92.7 kB
├ chunks/framework.js 45.2 kB
├ chunks/main.js 25.8 kB
├ chunks/pages/_app.js 21.5 kB ← Lodash 全体が含まれている
└ chunks/webpack.js 2.2 kB
Lodash 全体(約 24KB)が含まれているため、_app.js のサイズが大きくなっていますね。
lodash-es への移行
それでは、lodash-es を使ってツリーシェイクを有効にしましょう。
パッケージの入れ替え
bashyarn remove lodash
yarn add lodash-es
yarn add -D @types/lodash-es
コードの書き換え
pages/index.tsx を以下のように修正します。
typescript// pages/index.tsx
import { chain } from 'lodash-es';
import { useState } from 'react';
export default function Home() {
const [items, setItems] = useState([1, 2, 3, 4, 5]);
// lodash-es の chain を使用
const processedItems = chain(items)
.map((n) => n * 2)
.filter((n) => n > 5)
.value();
return (
<div>
<h1>処理結果</h1>
<ul>
{processedItems.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
</div>
);
}
デフォルト import(import _)を、必要な関数だけの名前付き import(import { chain })に変更しました。
ビルド結果の確認
再度ビルドすると、バンドルサイズが削減されていることが確認できます。
bashyarn build
textPage Size First Load JS
┌ ○ / 2.3 kB 75.8 kB
└ ○ /404 194 B 73.5 kB
+ First Load JS shared by all 73.3 kB
├ chunks/framework.js 45.2 kB
├ chunks/main.js 25.8 kB
├ chunks/pages/_app.js 2.1 kB ← 大幅に削減!
└ chunks/webpack.js 2.2 kB
_app.js のサイズが 21.5 kB → 2.1 kB に減りました。約 90% の削減 ですね!
webpack Bundle Analyzer での確認
視覚的にバンドルサイズを確認するために、webpack-bundle-analyzer を使ってみましょう。
インストールと設定
bashyarn add -D @next/bundle-analyzer
next.config.js に設定を追加します。
javascript// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')(
{
enabled: process.env.ANALYZE === 'true',
}
);
module.exports = withBundleAnalyzer({
// 既存の Next.js 設定
});
実行と結果
以下のコマンドで、バンドルの可視化を実行します。
bashANALYZE=true yarn build
ブラウザが自動的に開き、以下のような結果が表示されます。
最適化前(lodash 使用時):
- Lodash 全体が含まれ、約 70KB のモジュールが表示される
- 使っていない関数(
sortBy、groupByなど)も含まれている
最適化後(lodash-es 使用時):
chain、map、filterなど、使用している関数のみが表示される- モジュールサイズが約 2-3KB に削減
下図は、バンドル最適化前後の違いを示しています。
mermaidflowchart TB
subgraph before["最適化前(lodash)"]
lodash["lodash<br/>70KB"] --> all["全関数<br/>200+ functions"]
end
subgraph after["最適化後(lodash-es)"]
lodashes["lodash-es<br/>2-3KB"] --> used["使用関数のみ<br/>chain, map, filter"]
end
before -.->|ツリーシェイク<br/>適用| after
style before fill:#ffebee
style after fill:#e8f5e9
図で理解できる要点:
- 最適化により、バンドルサイズが 約 95% 削減 された
- 使っていない関数が自動的に除外された
- ツリーシェイクは、ES Modules(
lodash-es)で機能する
TypeScript での型の扱い
lodash-es を使う際の TypeScript の型定義について補足します。
型定義のインストール
bashyarn add -D @types/lodash-es
型推論の例
lodash-es では、各関数の型が正確に推論されます。
typescriptimport { map, filter, chunk } from 'lodash-es';
// ✅ 型推論が正常に動作
const numbers: number[] = [1, 2, 3, 4, 5];
const doubled = map(numbers, (n) => n * 2); // number[]
const filtered = filter(doubled, (n) => n > 5); // number[]
const chunked = chunk(filtered, 2); // number[][]
// ❌ 型エラーが検出される
const invalid = map(numbers, (n) => n.toUpperCase());
// Error: Property 'toUpperCase' does not exist on type 'number'
TypeScript を使っている場合、lodash-es の方が型の扱いが安全で便利です。
Vite プロジェクトでの適用
Vite を使っている場合も、lodash-es でツリーシェイクが有効になります。
インストール
bashyarn add lodash-es
yarn add -D @types/lodash-es
使用例
typescript// src/utils/data.ts
import { groupBy, sortBy } from 'lodash-es';
export function processData<T>(data: T[], key: keyof T) {
const grouped = groupBy(data, key);
return sortBy(Object.values(grouped), 'length');
}
ビルドサイズの確認
Vite のビルドコマンドを実行すると、自動的にツリーシェイクが適用されます。
bashyarn build
textvite v4.0.0 building for production...
✓ 42 modules transformed.
dist/index.html 0.45 kB
dist/assets/index-a3b2c1d4.js 2.31 kB ← lodash-es の必要な関数のみ
dist/assets/index-e4f5g6h7.css 1.23 kB
Vite は標準で Rollup を使っており、ES Modules のツリーシェイクが効率的に動作しますね。
まとめ
Lodash のツリーシェイクを有効にすることで、バンドルサイズを大幅に削減できることがわかりました。最後に、重要なポイントをまとめておきます。
重要なポイント
| # | ポイント | 内容 |
|---|---|---|
| 1 | 標準の lodash はツリーシェイク不可 | CommonJS 形式のため、バンドラがツリーシェイクできない |
| 2 | lodash-es を使う | ES Modules 版なので、モダンバンドラで最適化される |
| 3 | 個別 import も有効 | import map from 'lodash/map' で確実に削減できる |
| 4 | バンドル分析を定期的に実施 | webpack-bundle-analyzer などで可視化する |
| 5 | 新規プロジェクトでは lodash-es | 初めから最適化された形で開発を進められる |
削減効果のまとめ
以下の表は、各アプローチでのバンドルサイズの削減効果です。
| # | アプローチ | バンドルサイズ | 削減率 |
|---|---|---|---|
| 1 | import _ from 'lodash' | 70KB | 0% |
| 2 | import { map } from 'lodash' | 70KB | 0% |
| 3 | import map from 'lodash/map' | 6KB | 91% |
| 4 | import { map } from 'lodash-es' | 2KB | 97% |
次のステップ
Lodash のツリーシェイクを実装した後は、以下のステップに進むことをおすすめします。
- 他のライブラリも確認する: moment.js や date-fns など、他のライブラリでも同様の最適化ができます
- コード分割を導入する: Dynamic Import を使って、必要なときだけモジュールを読み込む
- 定期的にバンドル分析を実施する: CI/CD に組み込んで、バンドルサイズの推移を監視する
- パフォーマンス計測を行う: Lighthouse や WebPageTest でユーザー体験を定量的に評価する
適切なツリーシェイクにより、ユーザーにより速く、快適なアプリケーションを提供できるようになりますね。
関連リンク
articleLodash のツリーシェイクが効かない問題を解決:import 形態とバンドラ設定
articleLodash vs Ramda vs Rambda:パイプライン記法・カリー化・DX を徹底比較
articleLodash で管理画面テーブルを強化:並び替え・フィルタ・ページングの骨格
articleLodash を“薄いヘルパー層”として包む:プロジェクト固有ユーティリティの設計指針
articleLodash で巨大 JSON を“正規化 → 集計 → 整形”する 7 ステップ実装
articleLodash クイックレシピ :配列・オブジェクト変換の“定番ひな形”集
articlePrisma 読み書き分離設計:読み取りレプリカ/プロキシ/整合性モデルを整理
articleMermaid で日本語が潰れる問題を解決:フォント・エンコード・SVG 設定の勘所
articlePinia 2025 アップデート総まとめ:非互換ポイントと安全な移行チェックリスト
articleMCP サーバー 実装比較:Node.js/Python/Rust の速度・DX・コストをベンチマーク検証
articleLodash のツリーシェイクが効かない問題を解決:import 形態とバンドラ設定
articleOllama のインストール完全ガイド:macOS/Linux/Windows(WSL)対応手順
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来