Lodash-es と lodash の違いを理解してプロジェクトに最適導入
JavaScript の開発において、Lodash は配列やオブジェクト操作を効率化する強力なユーティリティライブラリとして広く使われています。しかし、npm で検索すると lodash と lodash-es という 2 つのパッケージが見つかり、どちらを選ぶべきか迷った経験はありませんか。
実は、この 2 つのパッケージには重要な違いがあり、プロジェクトの構成やバンドルサイズに大きく影響します。本記事では、lodash と lodash-es の違いを明確にし、あなたのプロジェクトに最適な導入方法を丁寧に解説いたします。
背景
Lodash とは
Lodash は、JavaScript で配列、オブジェクト、文字列などのデータ操作を簡単に行うためのユーティリティライブラリです。2012 年の登場以来、多くの開発者に愛用されており、GitHub では 5 万以上のスターを獲得しています。
javascript// Lodash を使った配列操作の例
import _ from 'lodash';
const users = [
{ name: '田中', age: 25 },
{ name: '佐藤', age: 30 },
{ name: '鈴木', age: 25 },
];
// 年齢でグループ化
const grouped = _.groupBy(users, 'age');
console.log(grouped);
// { 25: [{name: '田中', age: 25}, {name: '鈴木', age: 25}], 30: [{name: '佐藤', age: 30}] }
Lodash が提供する関数は 300 以上にのぼり、日常的な開発作業を大幅に効率化できます。
モジュールシステムの進化
JavaScript のモジュールシステムは、時代とともに大きく進化してきました。以下の図は、その変遷を示しています。
mermaidflowchart TB
era1["2009年以前<br/>グローバル変数時代"] -->|課題| era2["2009年<br/>CommonJS登場"]
era2 -->|Node.js標準化| era3["2015年<br/>ES Modules登場"]
era3 -->|ブラウザ対応| era4["現在<br/>ESM主流化"]
era2 -.->|使用| cjs["require/module.exports"]
era3 -.->|使用| esm["import/export"]
図の要点:
- CommonJS は Node.js の標準として普及
- ES Modules はブラウザネイティブ対応を実現
- 現代では ESM がモダンな標準として定着
CommonJS は require() と module.exports を使う方式で、主に Node.js で使われてきました。一方、ES Modules(ESM)は import と export を使う方式で、2015 年の ECMAScript 2015(ES6)で標準化されました。
| # | 項目 | CommonJS | ES Modules |
|---|---|---|---|
| 1 | 読み込み方式 | 動的(実行時) | 静的(ビルド時) |
| 2 | 構文 | require() / module.exports | import / export |
| 3 | ツリーシェイキング | 不可 | 可能 |
| 4 | ブラウザ対応 | 要バンドラー | ネイティブ対応 |
| 5 | 主な用途 | Node.js | モダン Web 開発 |
ツリーシェイキングとは、使用していないコードを自動的に削除してバンドルサイズを削減する技術です。これは ESM の静的な構造により実現可能となりました。
Lodash のパッケージ展開
Lodash の開発チームは、このモジュールシステムの進化に対応するため、複数のパッケージを提供しています。
javascript// CommonJS 形式(lodash パッケージ)
const _ = require('lodash');
const result = _.chunk([1, 2, 3, 4], 2);
javascript// ES Modules 形式(lodash-es パッケージ)
import { chunk } from 'lodash-es';
const result = chunk([1, 2, 3, 4], 2);
このように、同じ機能を提供しながら、モジュール形式が異なる 2 つのパッケージが存在するのです。では、具体的にどのような違いがあるのでしょうか。
課題
バンドルサイズの肥大化問題
モダンな Web アプリケーション開発において、バンドルサイズは非常に重要な問題です。ページの読み込み速度はユーザー体験に直結し、SEO にも影響を与えます。
javascript// 問題のあるインポート方法
import _ from 'lodash';
// 実際には chunk 関数しか使っていない
const result = _.chunk([1, 2, 3, 4], 2);
上記のコードでは、chunk 関数しか使っていないにもかかわらず、Lodash 全体(約 70KB gzip 圧縮後)がバンドルに含まれてしまいます。
以下の図は、不適切なインポートがバンドルサイズに与える影響を示しています。
mermaidflowchart LR
app["アプリケーション<br/>コード"] -->|import _ from| lodash["lodash全体<br/>70KB"]
app -->|実際の使用| chunk["chunk関数のみ<br/>1KB"]
lodash -->|バンドルに含まれる| bundle["最終バンドル<br/>69KB無駄"]
chunk -.->|本来必要なのは| ideal["理想のバンドル<br/>1KB"]
style bundle fill:#ffcccc
style ideal fill:#ccffcc
図で理解できる要点:
- 全体インポートは必要以上のコードをバンドルに含める
- 実際に使用する関数は全体のごく一部
- 無駄なコードがユーザーの読み込み速度を低下させる
ツリーシェイキングが効かない
Webpack や Rollup などのモダンバンドラーは、ツリーシェイキング機能を備えています。しかし、CommonJS 形式の lodash パッケージでは、この機能が十分に働きません。
javascript// lodash(CommonJS)の場合
import _ from 'lodash';
// → バンドラーは全体を含める必要があると判断
javascript// lodash-es の場合
import { chunk } from 'lodash-es';
// → バンドラーは chunk 関数のみを含めることができる
なぜ CommonJS ではツリーシェイキングが効かないのでしょうか。それは、CommonJS が動的な読み込みを許可しているためです。
javascript// CommonJS は実行時に動的な読み込みが可能
const functionName = Math.random() > 0.5 ? 'chunk' : 'map';
const fn = require('lodash')[functionName];
このような動的な使い方が可能なため、バンドラーは「どの関数が使われるか」を事前に判断できず、安全のために全てを含める必要があるのです。
パッケージ選択の混乱
npm で lodash を検索すると、以下のような複数のパッケージが見つかります。
| # | パッケージ名 | 週間ダウンロード数 | 説明 |
|---|---|---|---|
| 1 | lodash | 約 7000 万 | CommonJS 形式 |
| 2 | lodash-es | 約 1500 万 | ES Modules 形式 |
| 3 | lodash.chunk | 約 5 万 | 個別関数パッケージ |
| 4 | @types/lodash | 約 3000 万 | TypeScript 型定義 |
初心者の方は「どれを選べば良いのか」「複数インストールする必要があるのか」といった疑問を持たれることも多いでしょう。
また、既存プロジェクトに後から参加した場合、なぜ lodash が使われているのか、lodash-es に移行すべきかどうかの判断も難しい課題となります。
解決策
lodash と lodash-es の違いを理解する
それでは、2 つのパッケージの違いを明確に整理しましょう。
モジュール形式の違い
javascript// lodash(CommonJS 形式)
// ファイル内部: module.exports = { chunk: function() {...}, map: function() {...} }
const _ = require('lodash');
const { chunk } = require('lodash');
javascript// lodash-es(ES Modules 形式)
// ファイル内部: export function chunk() {...} export function map() {...}
import _ from 'lodash-es';
import { chunk } from 'lodash-es';
この違いにより、バンドラーの最適化能力が大きく変わります。
パッケージサイズとツリーシェイキング効果
実際のプロジェクトでバンドルサイズを比較してみましょう。
javascript// テストコード: chunk 関数のみを使用
import { chunk } from 'lodash-es';
const data = [1, 2, 3, 4, 5, 6];
const result = chunk(data, 2);
console.log(result);
上記コードをビルドした場合のバンドルサイズは以下のようになります。
| # | パッケージ | インポート方法 | バンドルサイズ(gzip) | 削減率 |
|---|---|---|---|---|
| 1 | lodash | import _ from 'lodash' | 約 70KB | - |
| 2 | lodash | import { chunk } from 'lodash' | 約 70KB | 0% |
| 3 | lodash-es | import _ from 'lodash-es' | 約 70KB | 0% |
| 4 | lodash-es | import { chunk } from 'lodash-es' | 約 2KB | ★ 97% |
重要なポイント: lodash-es で名前付きインポートを使った場合のみ、劇的なサイズ削減が実現できます。
TypeScript との親和性
TypeScript プロジェクトでは、型定義の扱いにも違いがあります。
typescript// lodash の場合は型定義パッケージが必要
import _ from 'lodash';
// yarn add -D @types/lodash が必要
typescript// lodash-es も型定義パッケージが必要
import { chunk } from 'lodash-es';
// yarn add -D @types/lodash-es が必要
両方とも DefinitelyTyped から型定義を取得する必要がありますが、lodash-es の型定義は ESM に最適化されており、より正確な型推論が可能です。
プロジェクトに応じた選択基準
では、どのような基準でパッケージを選べば良いのでしょうか。以下のフローチャートを参考にしてください。
mermaidflowchart TD
start["Lodashを<br/>導入したい"] --> q1{"モダンバンドラー<br/>使用?"}
q1 -->|Yes| q2{"ツリーシェイキング<br/>重視?"}
q1 -->|No| choice1["lodash"]
q2 -->|Yes| choice2["lodash-es<br/>+ 名前付きimport"]
q2 -->|No| q3{"Node.jsのみ?"}
q3 -->|Yes| choice1
q3 -->|No| choice2
choice1 -.->|結果| result1["バンドルサイズ大<br/>互換性高"]
choice2 -.->|結果| result2["バンドルサイズ小<br/>モダン"]
style choice2 fill:#ccffcc
style result2 fill:#ccffcc
図で理解できる要点:
- モダンな Web アプリケーションでは
lodash-esが推奨 - Node.js のみの環境では
lodashでも問題なし - ツリーシェイキングの効果を最大化するには名前付きインポートが必須
以下の表で、プロジェクトタイプごとの推奨パッケージをまとめました。
| # | プロジェクトタイプ | 推奨パッケージ | 理由 |
|---|---|---|---|
| 1 | Next.js / React / Vue | lodash-es | ツリーシェイキング効果大 |
| 2 | Node.js CLI ツール | lodash | CommonJS が標準 |
| 3 | Nuxt.js(SSR) | lodash-es | クライアントバンドル最適化 |
| 4 | Express サーバー | lodash | サーバーサイドのみ |
| 5 | TypeScript ライブラリ | lodash-es | ESM 出力に対応 |
導入方法のベストプラクティス
それでは、実際の導入手順を見ていきましょう。
lodash-es のインストール
bash# lodash-es をインストール
yarn add lodash-es
# TypeScript を使用する場合は型定義も追加
yarn add -D @types/lodash-es
Yarn を使うことで、依存関係を効率的に管理できます。
推奨されるインポート方法
javascript// ❌ 避けるべき方法: デフォルトインポート
import _ from 'lodash-es';
const result = _.chunk([1, 2, 3, 4], 2);
// → Lodash 全体がバンドルに含まれる
javascript// ✅ 推奨される方法: 名前付きインポート
import { chunk, groupBy, debounce } from 'lodash-es';
const chunked = chunk([1, 2, 3, 4], 2);
const grouped = groupBy(users, 'age');
// → 使用する関数のみがバンドルに含まれる
この違いは非常に重要です。必ず名前付きインポートを使用してください。
TypeScript での型安全な使用
TypeScript プロジェクトでは、型定義を活用することで、より安全なコードが書けます。
typescript// 型定義パッケージのインストール後
import { chunk, ChunkArray } from 'lodash-es';
// 型推論が正しく機能
const numbers: number[] = [1, 2, 3, 4, 5, 6];
const chunked = chunk(numbers, 2);
// chunked の型は number[][] と推論される
typescript// 独自の型との組み合わせ
interface User {
id: number;
name: string;
age: number;
}
import { groupBy } from 'lodash-es';
const users: User[] = [
{ id: 1, name: '田中', age: 25 },
{ id: 2, name: '佐藤', age: 30 },
];
const grouped = groupBy(users, 'age');
// grouped の型は _.Dictionary<User[]> と推論される
型定義により、エディタの補完機能も強化され、開発効率が向上します。
具体例
React プロジェクトでの実装例
実際の React プロジェクトで lodash-es を活用する例を見ていきましょう。
プロジェクトのセットアップ
bash# React プロジェクトの作成
yarn create vite my-app --template react-ts
cd my-app
# lodash-es と型定義のインストール
yarn add lodash-es
yarn add -D @types/lodash-es
Vite は高速なビルドツールで、ESM を標準としているため、lodash-es との相性が抜群です。
検索機能の実装
ユーザー検索機能を実装する例です。デバウンス処理を使って、入力のたびに検索が走らないように最適化します。
typescript// src/hooks/useUserSearch.ts
import { useState, useEffect } from 'react';
import { debounce } from 'lodash-es';
interface User {
id: number;
name: string;
email: string;
}
export const useUserSearch = (users: User[]) => {
const [searchTerm, setSearchTerm] = useState('');
const [filteredUsers, setFilteredUsers] =
useState<User[]>(users);
return { searchTerm, setSearchTerm, filteredUsers };
};
typescript// デバウンス処理の実装
useEffect(() => {
// 300ms の遅延でデバウンス
const debouncedSearch = debounce((term: string) => {
const filtered = users.filter(
(user) =>
user.name
.toLowerCase()
.includes(term.toLowerCase()) ||
user.email
.toLowerCase()
.includes(term.toLowerCase())
);
setFilteredUsers(filtered);
}, 300);
debouncedSearch(searchTerm);
// クリーンアップ
return () => {
debouncedSearch.cancel();
};
}, [searchTerm, users]);
debounce 関数により、ユーザーが入力を終えるまで検索処理を遅延させ、パフォーマンスを改善しています。
コンポーネントでの使用
typescript// src/components/UserSearch.tsx
import React from 'react';
import { useUserSearch } from '../hooks/useUserSearch';
interface Props {
users: User[];
}
export const UserSearch: React.FC<Props> = ({ users }) => {
const { searchTerm, setSearchTerm, filteredUsers } =
useUserSearch(users);
return (
<div>
<input
type='text'
placeholder='ユーザーを検索...'
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
);
};
typescript// 検索結果の表示
return (
<div>
{/* 入力フィールド... */}
<ul>
{filteredUsers.map((user) => (
<li key={user.id}>
{user.name} - {user.email}
</li>
))}
</ul>
<p>検索結果: {filteredUsers.length} 件</p>
</div>
);
このように、debounce 関数だけがバンドルに含まれるため、バンドルサイズを最小限に抑えられます。
データ整形とグループ化の例
次に、複雑なデータ操作の例を見てみましょう。
データの準備
typescript// src/data/salesData.ts
export interface SaleRecord {
id: number;
productName: string;
category: string;
price: number;
quantity: number;
date: string;
}
export const salesData: SaleRecord[] = [
{
id: 1,
productName: 'ノート PC',
category: '電子機器',
price: 120000,
quantity: 2,
date: '2024-01-15',
},
{
id: 2,
productName: 'マウス',
category: '周辺機器',
price: 3000,
quantity: 5,
date: '2024-01-16',
},
{
id: 3,
productName: 'キーボード',
category: '周辺機器',
price: 8000,
quantity: 3,
date: '2024-01-16',
},
{
id: 4,
productName: 'モニター',
category: '電子機器',
price: 35000,
quantity: 1,
date: '2024-01-17',
},
];
このデータをカテゴリ別に集計してみます。
カテゴリ別集計の実装
typescript// src/utils/salesAnalysis.ts
import { groupBy, sumBy, meanBy } from 'lodash-es';
import { SaleRecord } from '../data/salesData';
export const analyzeSalesByCategory = (
sales: SaleRecord[]
) => {
// カテゴリでグループ化
const grouped = groupBy(sales, 'category');
return grouped;
};
typescript// 各カテゴリの統計情報を計算
export const calculateCategoryStats = (
sales: SaleRecord[]
) => {
const grouped = groupBy(sales, 'category');
const stats = Object.entries(grouped).map(
([category, records]) => ({
category,
totalRevenue: sumBy(
records,
(r) => r.price * r.quantity
),
averagePrice: meanBy(records, 'price'),
totalQuantity: sumBy(records, 'quantity'),
itemCount: records.length,
})
);
return stats;
};
groupBy、sumBy、meanBy の 3 つの関数のみがバンドルに含まれます。
結果の表示コンポーネント
typescript// src/components/SalesReport.tsx
import React from 'react';
import { calculateCategoryStats } from '../utils/salesAnalysis';
import { salesData } from '../data/salesData';
export const SalesReport: React.FC = () => {
const stats = calculateCategoryStats(salesData);
return (
<div>
<h2>カテゴリ別売上レポート</h2>
<table>
<thead>
<tr>
<th>カテゴリ</th>
<th>総売上</th>
<th>平均単価</th>
<th>総販売数</th>
</tr>
</thead>
</table>
</div>
);
};
typescript// テーブルボディの実装
return (
<tbody>
{stats.map((stat) => (
<tr key={stat.category}>
<td>{stat.category}</td>
<td>¥{stat.totalRevenue.toLocaleString()}</td>
<td>
¥{Math.round(stat.averagePrice).toLocaleString()}
</td>
<td>{stat.totalQuantity}</td>
</tr>
))}
</tbody>
);
このように、複雑なデータ操作も Lodash を使うことで簡潔に記述できますね。
バンドルサイズの検証
実際にビルドして、バンドルサイズを確認してみましょう。
bash# 本番ビルドの実行
yarn build
# ビルド結果の確認
yarn vite-bundle-visualizer
以下の図は、ビルド結果の構造を示しています。
mermaidflowchart TB
src["ソースコード<br/>src/"] --> build["ビルドプロセス<br/>Vite + Rollup"]
build --> chunk1["main.js<br/>アプリコード"]
build --> chunk2["vendor.js<br/>依存ライブラリ"]
chunk2 --> react["React: 45KB"]
chunk2 --> lodash["lodash-es使用分: 3KB"]
style lodash fill:#ccffcc
図で理解できる要点:
- 必要な関数のみがバンドルに含まれている
- Lodash 全体(70KB)ではなく、使用分のみ(3KB)
- 大幅なサイズ削減により、読み込み速度が向上
ビルド結果の例を以下に示します。
| # | ファイル | サイズ(gzip) | 内容 |
|---|---|---|---|
| 1 | main.js | 15KB | アプリケーションコード |
| 2 | vendor.js | 48KB | React + lodash-es 使用分 |
| 3 | 合計 | 63KB | 全体バンドルサイズ |
もし lodash 全体インポートを使っていた場合、vendor.js は 115KB 程度になり、約 67KB の差が生まれます。
Next.js での実装例
Next.js プロジェクトでも同様に lodash-es を活用できます。
サーバーコンポーネントでの使用
typescript// app/users/page.tsx (Server Component)
import { groupBy, sortBy } from 'lodash-es';
interface User {
id: number;
name: string;
department: string;
joinDate: string;
}
async function getUsers(): Promise<User[]> {
// API からユーザーデータを取得
const res = await fetch('https://api.example.com/users');
return res.json();
}
typescript// ページコンポーネント
export default async function UsersPage() {
const users = await getUsers();
// 部署でグループ化
const usersByDept = groupBy(users, 'department');
// 各部署内で入社日順にソート
const sortedDepts = Object.entries(usersByDept).map(
([dept, deptUsers]) => ({
department: dept,
users: sortBy(deptUsers, 'joinDate'),
})
);
return (
<div>
<h1>部署別社員一覧</h1>
{/* レンダリング処理... */}
</div>
);
}
Server Component では、この処理がサーバー側で実行されるため、クライアントのバンドルサイズには影響しません。
クライアントコンポーネントでの使用
typescript// app/components/SearchFilter.tsx
'use client';
import { useState } from 'react';
import { debounce, filter } from 'lodash-es';
interface Props {
items: string[];
onFilterChange: (filtered: string[]) => void;
}
export function SearchFilter({
items,
onFilterChange,
}: Props) {
const [query, setQuery] = useState('');
return (
<input
type='text'
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder='検索...'
/>
);
}
typescript// デバウンス処理の実装
const handleSearch = debounce((searchQuery: string) => {
const filtered = filter(items, (item) =>
item.toLowerCase().includes(searchQuery.toLowerCase())
);
onFilterChange(filtered);
}, 300);
// useEffect で検索を実行
useEffect(() => {
handleSearch(query);
return () => handleSearch.cancel();
}, [query]);
Client Component では、debounce と filter の 2 つの関数のみがクライアントバンドルに含まれます。
まとめ
本記事では、lodash と lodash-es の違いを詳しく解説してまいりました。重要なポイントを振り返りましょう。
パッケージの違い:
lodashは CommonJS 形式で、主に Node.js 環境向けlodash-esは ES Modules 形式で、モダンバンドラーでのツリーシェイキングに対応- 同じ機能を提供するが、モジュール形式の違いがバンドルサイズに大きく影響
選択の基準:
- モダンな Web アプリケーション(React、Vue、Next.js など)では
lodash-esを選択 - Node.js のみの環境では
lodashでも問題なし - 必ず名前付きインポートを使用してツリーシェイキングの効果を最大化
導入のベストプラクティス:
yarn add lodash-esでインストール- TypeScript では
@types/lodash-esも追加 import { 関数名 } from 'lodash-es'の形式でインポート- デフォルトインポートは避ける
適切なパッケージ選択により、バンドルサイズを最大 97% 削減でき、ページの読み込み速度を大幅に改善できます。ユーザー体験の向上と SEO 対策の両面で、非常に重要な最適化となるでしょう。
あなたのプロジェクトでも、ぜひ lodash-es と名前付きインポートを活用して、高速で効率的なアプリケーションを構築してください。
関連リンク
articleLodash-es と lodash の違いを理解してプロジェクトに最適導入
articleLodash を使う/使わない判断基準:2025 年のネイティブ API と併用戦略
articleLodash の組織運用ルール:no-restricted-imports と コーディング規約の設計
articleLodash のツリーシェイクが効かない問題を解決:import 形態とバンドラ設定
articleLodash vs Ramda vs Rambda:パイプライン記法・カリー化・DX を徹底比較
articleLodash で管理画面テーブルを強化:並び替え・フィルタ・ページングの骨格
articleNotebookLM 活用事例:営業提案書の下書きと顧客要件の整理を自動化
articleGrok RAG 設計入門:社内ドキュメント検索を高精度にする構成パターン
articlegpt-oss 運用監視ダッシュボード設計:Prometheus/Grafana/OTel で可観測性強化
articleNode.js 標準テストランナー完全理解:`node:test` がもたらす新しい DX
articleNext.js の Route Handlers で multipart/form-data が受け取れない問題の切り分け術
articleMCP サーバー で社内ナレッジ検索チャットを構築:権限制御・要約・根拠表示の実装パターン
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 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来