T-CREATOR

Lodash-es と lodash の違いを理解してプロジェクトに最適導入

Lodash-es と lodash の違いを理解してプロジェクトに最適導入

JavaScript の開発において、Lodash は配列やオブジェクト操作を効率化する強力なユーティリティライブラリとして広く使われています。しかし、npm で検索すると lodashlodash-es という 2 つのパッケージが見つかり、どちらを選ぶべきか迷った経験はありませんか。

実は、この 2 つのパッケージには重要な違いがあり、プロジェクトの構成やバンドルサイズに大きく影響します。本記事では、lodashlodash-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)は importexport を使う方式で、2015 年の ECMAScript 2015(ES6)で標準化されました。

#項目CommonJSES Modules
1読み込み方式動的(実行時)静的(ビルド時)
2構文require() / module.exportsimport / 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 を検索すると、以下のような複数のパッケージが見つかります。

#パッケージ名週間ダウンロード数説明
1lodash約 7000 万CommonJS 形式
2lodash-es約 1500 万ES Modules 形式
3lodash.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)削減率
1lodashimport _ from 'lodash'約 70KB-
2lodashimport { chunk } from 'lodash'約 70KB0%
3lodash-esimport _ from 'lodash-es'約 70KB0%
4lodash-esimport { 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 でも問題なし
  • ツリーシェイキングの効果を最大化するには名前付きインポートが必須

以下の表で、プロジェクトタイプごとの推奨パッケージをまとめました。

#プロジェクトタイプ推奨パッケージ理由
1Next.js / React / Vuelodash-esツリーシェイキング効果大
2Node.js CLI ツールlodashCommonJS が標準
3Nuxt.js(SSR)lodash-esクライアントバンドル最適化
4Express サーバーlodashサーバーサイドのみ
5TypeScript ライブラリlodash-esESM 出力に対応

導入方法のベストプラクティス

それでは、実際の導入手順を見ていきましょう。

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;
};

groupBysumBymeanBy の 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)内容
1main.js15KBアプリケーションコード
2vendor.js48KBReact + 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 では、debouncefilter の 2 つの関数のみがクライアントバンドルに含まれます。

まとめ

本記事では、lodashlodash-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 と名前付きインポートを活用して、高速で効率的なアプリケーションを構築してください。

関連リンク