T-CREATOR

Lodash のツリーシェイクが効かない問題を解決:import 形態とバンドラ設定

Lodash のツリーシェイクが効かない問題を解決:import 形態とバンドラ設定

JavaScript プロジェクトで Lodash を使っていると、バンドルサイズが予想以上に大きくなってしまうことがありますよね。実は、Lodash の import の仕方やバンドラの設定によって、使っていない関数まで含まれてしまうことが原因なんです。

この記事では、Lodash のツリーシェイクが効かない理由と、具体的な解決策をご紹介します。正しい import 形態とバンドラ設定を理解すれば、バンドルサイズを劇的に削減できますよ。

背景

Lodash とは

Lodash は、JavaScript で配列やオブジェクトの操作を簡単に行える便利なユーティリティライブラリです。mapfilterdebounce など、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);

この場合、mapfilter だけを使いたいのに、Lodash のすべての関数がバンドルされてしまうんです。

課題

バンドルサイズの肥大化

Lodash 全体をバンドルに含めてしまうと、以下のような問題が発生します。

#課題影響
1バンドルサイズの増加初回ロード時間が長くなり、ユーザー体験が低下する
2ネットワーク転送量の増加モバイル環境などで通信コストが増える
3パース・実行時間の増加JavaScript の解析と実行に時間がかかり、表示が遅れる
4キャッシュ効率の低下不要なコードが含まれるため、キャッシュが非効率になる

実際のバンドルサイズ比較

以下の表は、異なる import 形式でのバンドルサイズの違いを示しています。

#import 形式バンドルサイズ(圧縮前)バンドルサイズ(gzip 後)
1import _ from 'lodash'70KB24KB
2import { map } from 'lodash'70KB24KB
3import map from 'lodash​/​map'6KB2KB
4import { map } from 'lodash-es'2KB0.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);

これで、mapfilter だけがバンドルに含まれるようになります。デフォルト import(import _)を使っている箇所は、名前付き import に変更する必要があります。

ステップ 3:バンドラ設定の確認

webpack を使っている場合、package.jsonsideEffects の設定を確認しましょう。

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 を導入
4Lodash の使用箇所が少ない個別 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 のモジュールが表示される
  • 使っていない関数(sortBygroupBy など)も含まれている

最適化後(lodash-es 使用時):

  • chainmapfilter など、使用している関数のみが表示される
  • モジュールサイズが約 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 形式のため、バンドラがツリーシェイクできない
2lodash-es を使うES Modules 版なので、モダンバンドラで最適化される
3個別 import も有効import map from 'lodash​/​map' で確実に削減できる
4バンドル分析を定期的に実施webpack-bundle-analyzer などで可視化する
5新規プロジェクトでは lodash-es初めから最適化された形で開発を進められる

削減効果のまとめ

以下の表は、各アプローチでのバンドルサイズの削減効果です。

#アプローチバンドルサイズ削減率
1import _ from 'lodash'70KB0%
2import { map } from 'lodash'70KB0%
3import map from 'lodash​/​map'6KB91%
4import { map } from 'lodash-es'2KB97%

次のステップ

Lodash のツリーシェイクを実装した後は、以下のステップに進むことをおすすめします。

  1. 他のライブラリも確認する: moment.js や date-fns など、他のライブラリでも同様の最適化ができます
  2. コード分割を導入する: Dynamic Import を使って、必要なときだけモジュールを読み込む
  3. 定期的にバンドル分析を実施する: CI/CD に組み込んで、バンドルサイズの推移を監視する
  4. パフォーマンス計測を行う: Lighthouse や WebPageTest でユーザー体験を定量的に評価する

適切なツリーシェイクにより、ユーザーにより速く、快適なアプリケーションを提供できるようになりますね。

関連リンク