Node.jsの開発環境において、モジュールシステムの理解は欠かすことのできない重要な要素です。
古くから利用されてきたCommonJS(CJS)と、現在の標準であるECMAScript Modules(ESM)には、設計思想から実行挙動に至るまで多くの違いが存在します。
2026年現在のNode.jsエコシステムでは、ESMへの完全な移行が進む一方で、既存のプロジェクトや特定のライブラリでは依然としてCommonJSが併用されています。
本記事では、これら2つのモジュールシステムの仕組みを深掘りし、エンジニアが直面する移行の課題や最適な使い分けについて詳しく解説します。
CommonJS (CJS) の基本構造と特徴
CommonJSは、Node.jsが誕生した当初から採用されてきたデフォルトのモジュールシステムです。
サーバーサイドでJavaScriptを動作させるために設計されており、ファイルの読み込みを同期的に行うことが最大の特徴です。
基本的な構文では、require()関数を使用してモジュールを読み込み、module.exportsを使用して外部に機能を公開します。
この仕組みは非常にシンプルであり、実行時に動的に読み込むモジュールを決定できるという柔軟性を持っています。
CommonJSのコード例
実際にCommonJSで記述されたコードを確認してみましょう。
// math.js
function add(a, b) {
return a + b;
}
// 外部に公開する
module.exports = {
add: add
};
// main.js
// モジュールを同期的に読み込む
const math = require('./math.js');
console.log(math.add(5, 10));
15
CommonJSでは、モジュールが読み込まれるまで次の行のコードは実行されません。
これはサーバーサイドのファイルシステムにおいては効率的でしたが、ネットワークを介してリソースを読み込むブラウザ環境には適していませんでした。
また、CJSは実行時に初めて依存関係が解決されるため、静的解析による最適化が難しいという側面があります。
ECMAScript Modules (ESM) の台頭とその役割
ECMAScript Modules(ESM)は、JavaScriptの言語仕様(ECMAScript)の一部として標準化されたモジュールシステムです。
Node.js v12付近から本格的に導入が始まり、2026年現在ではNode.jsにおける標準的な記述方式となっています。
ESMはimportおよびexportキーワードを使用し、ファイルの読み込みを非同期的に処理します。
これにより、ブラウザ環境との互換性が向上し、モダンなフロントエンド開発との親和性が極めて高くなりました。
ESMのコード例
次に、同じ処理をESMで記述した場合のコードを確認します。
// math.mjs
export function add(a, b) {
return a + b;
}
// main.mjs
import { add } from './math.mjs';
console.log(add(10, 20));
30
ESMの最大の特徴は、解析段階で依存関係を特定できる静的構造にあります。
これにより、使用されていないコードをビルド時に削除する「ツリーシェイキング」という最適化が可能になります。
モダンな開発現場では、パフォーマンスの観点からもESMの採用が強く推奨されています。
CommonJSとESMの決定的な違い
両者の違いを理解するためには、単なる構文の差異だけでなく、動作メカニズムの違いに注目する必要があります。
主要な相違点を以下の表にまとめました。
| 機能 | CommonJS (CJS) | ECMAScript Modules (ESM) |
|---|---|---|
| 読み込み構文 | require() | import / export |
| 読み込みタイミング | 同期 (実行時) | 非同期 (解析時) |
| Top-level Await | 非対応 | 対応 |
特殊変数 (__dirname等) | 使用可能 | 使用不可 (代替手段が必要) |
| ツリーシェイキング | 困難 | 容易 |
1. 読み込みのタイミングと順序
CommonJSは、コードが上から順に実行される過程でrequire()が呼ばれた瞬間にモジュールを読み込みます。
一方でESMは、コードの実行が始まる前にあらかじめ全てのimport文を解析し、モジュールの依存関係グラフを構築します。
このため、ESMでは条件分岐(if文)の中でimport文を使用することはできません(動的インポートを除く)。
2. 特殊変数の有無
CommonJSでは、現在のディレクトリパスを示す__dirnameや、ファイルパスを示す__filenameが自動的に提供されていました。
しかし、ESMにはこれらのグローバル変数が存在しません。
ESMで同様の情報を取得するには、import.meta.urlを利用してパスを変換する必要があります。
// ESMでのパス取得方法
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
console.log(__dirname);
3. Top-level Awaitの利用
ESMでは、非同期処理を待機するためのawaitを、関数の外(トップレベル)で直接記述することができます。
CommonJSでは必ずasync関数で囲う必要があったため、これはESMを採用する大きなメリットの一つです。
データベースへの接続待ちや、設定ファイルの非同期読み込みが非常に簡潔に記述できるようになります。
Node.jsでのESM有効化と設定方法
Node.jsでファイルをESMとして認識させるには、主に2つの方法があります。
一つは、拡張子を.mjsにすることです。
もう一つは、package.jsonに"type": "module"を記述する方法です。
package.jsonによる設定
プロジェクト全体をESMとして扱う場合は、以下の設定を推奨します。
{
"name": "my-project",
"version": "1.0.0",
"type": "module",
"dependencies": {
"express": "^5.0.0"
}
}
この設定を行うと、プロジェクト内の.jsファイルは全てESMとして解釈されます。
もし一部のファイルだけCommonJSとして動作させたい場合は、そのファイルの拡張子を.cjsにする必要があります。
相互運用性:CJSからESM、ESMからCJSの呼び出し
現代の開発において最も厄介なのが、異なるモジュールシステム間の連携です。
基本ルールとして、ESMからCommonJSを読み込むことは比較的容易ですが、その逆は困難を伴います。
ESMからCommonJSを呼び出す
ESM内では、import文を使って既存のCJSライブラリを読み込むことができます。
ただし、CJS側がmodule.exportsで一つのオブジェクトを返している場合、デフォルトインポートを使用する必要があります。
// ESM環境
import pkg from './legacy-cjs-module.cjs';
console.log(pkg.someFunction());
CommonJSからESMを呼び出す
CommonJSのrequire()は同期的なため、非同期であるESMを直接読み込むことはできません。
どうしても読み込みたい場合は、動的インポート(import())を使用する必要があります。
// CommonJS環境
async function loadESM() {
const { someFunction } = await import('./modern-esm-module.mjs');
someFunction();
}
loadESM();
この「同期と非同期の壁」が、古いプロジェクトをESMに移行する際の大きな障壁となっています。
移行のポイント:CommonJSからESMへ
プロジェクトをESMへ移行する際には、段階的なアプローチが必要です。
まずは、依存しているライブラリがESMに対応しているかを確認してください。
2026年現在、主要なライブラリのほとんどはESMをネイティブサポートしていますが、メンテナンスが止まっている古いライブラリには注意が必要です。
ステップ1:package.jsonの更新
前述の通り、"type": "module"を追加します。
これにより、Node.jsのランタイムが全てのファイルをESMとして扱い始めます。
ステップ2:構文の置換
requireをimportに、module.exportsをexportに書き換えます。
ここで重要なのは、ESMではファイル拡張子の省略が許されない(Node.jsのデフォルト設定の場合)点です。
import { func } from './utils' ではなく import { func } from './utils.js' と記述する必要があります。
ステップ3:グローバル変数の置換
__dirname や __filename を使用している箇所を、import.meta.url ベースの処理に修正します。
また、CJS特有の require.cache などの機能に依存しているコードも、ESMの仕様に合わせた設計変更が必要です。
2026年における最新のトレンドと推奨事項
2026年のNode.js開発においては、新規プロジェクトでCommonJSを選択する理由はほとんどありません。
クラウドネイティブな環境やエッジコンピューティング(Edge Runtime)では、ESMが前提となっているケースが多いためです。
また、TypeScriptを使用する場合も、出力形式をESM(NodeNext)に設定することが一般的になっています。
これまでは「モジュール解決の複雑さ」を理由にESMを避ける動きもありましたが、ツールチェーンの進化により、現在ではそのストレスも大幅に軽減されています。
ビルドツールやテストフレームワーク(Vitestや最新のJestなど)も、ESMを第一市民として扱うようになっています。
まとめ
Node.jsにおけるCommonJSとESMの違いは、単なる書き方の違いではなく、JavaScriptという言語が「標準化」に向かう過程で生まれた大きな進化の足跡です。
同期的なCommonJSは、その簡便さからNode.jsの初期の成功を支えましたが、モダンなWeb標準であるESMには、静的解析やパフォーマンス最適化といった強力な利点があります。
開発者は両者の違いを正確に理解し、既存資産を維持しつつも、新しい標準であるESMへと積極的にシフトしていくことが求められます。
特にパスの扱いやモジュールの読み込み順序といった細かな仕様差を把握しておくことは、トラブルシューティングの際にも大いに役立つはずです。
2026年の今、改めて自身のプロジェクトの構成を見直し、より洗練されたモジュール設計を目指していきましょう。
