Node.jsにおける開発において、モジュール読み込みの仕組みを正しく理解することは、効率的でメンテナンス性の高いアプリケーションを構築するための第一歩です。
2026年現在、Node.jsのエコシステムは大きな転換点を経て、ES Modules (ESM) とCommonJS (CJS) が共存しつつも、ESMへの移行がほぼ標準化された時代を迎えています。
本記事では、これら2つのモジュールシステムの内部構造から、最新のNode.js環境における最適な使い分け、そして相互運用のためのベストプラクティスを詳しく解説します。
最新の技術動向を踏まえた知識を整理し、現場で即座に役立つ実装指針を提供することを目指します。
Node.jsにおける2つのモジュールシステム
Node.jsには、古くから利用されているCommonJSと、JavaScriptの標準仕様として策定されたES Modulesの2種類が存在します。
CommonJSは、Node.jsが誕生した当初から採用されており、同期的なモジュール読み込みを特徴としています。
一方で、ES Modulesはブラウザ環境との互換性を考慮して設計されており、静的解析が可能であるという利点があります。
かつてのNode.jsではCommonJSが主流でしたが、現在は多くのライブラリがESMを優先的にサポートするようになっています。
開発者は、プロジェクトの要件や依存するライブラリに応じて、これら2つの挙動を正確に把握しておく必要があります。
CommonJS (CJS) の基本構造と特徴
CommonJSは、require()関数を使用して他のファイルを読み込み、module.exportsを使用して機能を公開する仕組みです。
このシステムは同期的に動作するため、ファイルが読み込まれるまで次の処理は実行されません。
この特性はサーバーサイドの起動時などには直感的で扱いやすい反面、大規模な依存関係がある場合にはパフォーマンスに影響を与える可能性があります。
以下のコードは、基本的なCommonJSの記述例です。
// math.js (CommonJS)
const add = (a, b) => a + b;
// module.exportsを使用して公開する
module.exports = {
add
};
// app.js (CommonJS)
// requireを使用して読み込む
const math = require('./math.js');
console.log(math.add(5, 3));
8
CommonJSでは、モジュールの読み込みはプログラムの実行時に評価されます。
そのため、条件分岐の中でrequire()を呼び出すといった動的な読み込みも容易に行えます。
ES Modules (ESM) の基本構造と特徴
ES Modulesは、import文とexport文を使用するモダンなモジュールシステムです。
ESMは静的に解析されるため、プログラムを実行する前に依存関係のグラフを構築することが可能です。
これにより、未使用のコードを削除する「ツリーシェイキング」などの最適化が容易になります。
また、ESMではトップレベルでのawaitがサポートされているため、非同期的な初期化処理も簡潔に記述できます。
以下のコードは、ES Modulesを使用した記述例です。
// math.mjs (ESM)
export const add = (a, b) => a + b;
// app.mjs (ESM)
import { add } from './math.mjs';
console.log(add(10, 20));
30
Node.jsにおいてESMを利用する場合、拡張子を.mjsにするか、package.jsonに"type": "module"を記述する必要があります。
ESMとCJSの主要な違い
これら2つのシステムには、単なる構文の違いだけでなく、動作原理においても大きな隔たりがあります。
特に変数のスコープや特殊なグローバル変数の有無については、移行時にトラブルの原因となりやすいポイントです。
以下の表で、主要な違いを整理しました。
| 機能 | CommonJS (CJS) | ES Modules (ESM) |
|---|---|---|
| 読み込み構文 | require() | import / export |
| 読み込みタイミング | 実行時の同期読み込み | パース時の静的解析 (一部動的) |
| 拡張子 | .cjs / .js | .mjs / .js (type:module時) |
__dirname / __filename | 利用可能 | 利用不可 (import.metaを使用) |
| トップレベルAwait | 利用不可 | 利用可能 |
特殊な変数の扱いに関する注意点
CommonJSで頻繁に利用されていた__dirnameや__filenameは、ESM環境では直接参照することができません。
ESMでファイルパス情報を取得するには、import.meta.urlを活用する必要があります。
これはファイルURL形式で情報を保持しているため、urlモジュールのfileURLToPath関数を使用してパスに変換するのが一般的です。
// ESMで__dirnameを再現する方法
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
console.log(__dirname);
このように、ESMではモダンな標準仕様に準拠するため、Node.js固有のグローバル変数が一部排除されていることに注意してください。
Node.js 2026年における最新のモジュール読み込み動作
Node.jsの近年のバージョンアップにより、ESMとCJSの相互運用性は劇的に向上しました。
以前はESMからCJSを呼び出すことは簡単でしたが、CJSからESMを呼び出すには非同期のimport()関数を使用するしかありませんでした。
しかし、最新のNode.jsではrequire()によるESMの読み込みが特定の条件下で試験的、あるいは安定的にサポートされるようになっています。
これにより、レガシーなプロジェクトに最新のライブラリを導入する際の障壁が低くなりました。
require(esm) の仕組みと制限
Node.jsの最新機能では、同期的なrequire()を使用してESMモジュールを読み込むことが可能です。
ただし、読み込まれるESMモジュール内にトップレベルのawaitが含まれていないことが条件となります。
トップレベルawaitを含むモジュールは本質的に非同期であるため、同期的なrequire()では解決できないためです。
この機能により、既存の膨大なCJSコードベースを維持しつつ、新しいESMモジュールを部分的に採用することが容易になりました。
// CJS環境からESMをrequireする例 (最新Node.js)
// ※esm-module.mjs がトップレベルawaitを含まない場合
const esmMod = require('./esm-module.mjs');
console.log(esmMod.someValue);
ただし、パフォーマンスや将来的な標準化の観点からは、可能な限りプロジェクト全体をESMへ移行することが推奨されます。
パッケージ開発におけるベストプラクティス
ライブラリなどのパッケージを開発し公開する場合、CJSとESMの両方のユーザーに対応することが求められます。
これを「デュアルパッケージ」と呼びますが、適切な構成を行わないと、同一モジュールの二重読み込みによる状態の不一致などが発生するリスクがあります。
package.json の exports フィールド活用
現代のNode.js開発において、mainフィールドだけでは不十分です。
exportsフィールドを使用することで、読み込み側がrequireを使用しているかimportを使用しているかに応じて、適切なエントリポイントを自動的に切り替えることができます。
{
"name": "my-library",
"version": "1.0.0",
"type": "module",
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
}
}
}
このように設定することで、利用者はモジュールの形式を意識することなく、最適なファイルを読み込むことが可能になります。
また、内部的なファイルを隠蔽し、公開するAPIを厳格に制御できるというメリットもあります。
Conditional Exportsの優先順位
exports内のキーには定義の順序が重要になる場合があります。
Node.jsは、定義された順番に条件をチェックし、最初にマッチしたものを採用します。
そのため、より具体的な条件を先に記述し、汎用的な条件を後に記述するのが鉄則です。
また、TypeScriptを使用している場合は、typesキーを最優先で記述することが推奨されます。
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
}
パフォーマンスへの影響と最適化
モジュール読み込みのパフォーマンスは、アプリケーションの起動速度に直結します。
CJSは読み込み時にファイルを同期的に読み取りますが、ネットワーク越しの読み込みが発生し得る環境ではESMの非同期性が有利に働きます。
Node.jsのローカル実行環境においても、ESMの静的解析によってV8エンジンが事前最適化を行いやすくなるため、長期的な実行パフォーマンスが向上する傾向にあります。
依存関係の最小化
モジュールシステムの種類に関わらず、読み込むモジュールの数は最小限に抑えるべきです。
特に深いネスト構造を持つ依存関係は、ファイルシステムのI/O負荷を高める原因となります。
2026年の開発環境では、ビルドツール(esbuildやRollupなど)を使用してモジュールを適切にバンドルし、実行時の読み込みオーバーヘッドを削減することが一般的です。
ただし、Node.jsネイティブの読み込み速度も改善されているため、開発環境ではバンドルなしで動作させ、本番環境のみ最適化するといった柔軟な運用が可能です。
トラブルシューティング:よくあるエラーと対処法
モジュール読み込みに関連するエラーは、初心者からベテランまで多くの開発者を悩ませます。
特に「Must use import to load ES Module」や「require() of ES Module is not supported」といったエラーは、設定の不整合が原因です。
ERR_REQUIRE_ESM への対応
このエラーは、ESMとして定義されたファイルをrequire()で読み込もうとした際に発生します。
前述の通り、最新のNode.jsでは条件付きで許可されていますが、基本的にはESM側を読み込む際にimport()を使用するか、呼び出し元もESMに変換する必要があります。
もしライブラリ側がESMのみを提供している場合、CJSプロジェクト側では動的インポートを採用するのが現実的な回避策です。
// CommonJSプロジェクト内でESMライブラリを読み込む方法
async function loadLibrary() {
const { someFunction } = await import('esm-only-package');
someFunction();
}
このように、非同期関数の中でラップすることで、CJSの同期的な制約を回避できます。
ERR_UNKNOWN_FILE_EXTENSION
このエラーは、Node.jsがファイルの拡張子をどのように解釈すればよいか判断できない場合に発生します。
特に.jsファイルを使用している場合、近接するpackage.jsonの"type"フィールドの設定を確認してください。
意図せずESMとして扱われている場合は、拡張子を.cjsに変更することで明示的にCommonJSとして認識させることができます。
逆に、ESMとして動作させたい場合は.mjsに変更するか、package.jsonに"type": "module"を追加しましょう。
まとめ
Node.jsにおけるモジュールシステムは、長年の進化を経てES Modulesを中心としたモダンな形に整理されました。
CommonJSは依然として強力な互換性を維持していますが、新規開発においてはES Modulesを選択することが将来的なメンテナンス性の観点から強く推奨されます。
2026年現在のNode.js環境では、exportsフィールドを適切に構成し、CJSとESMの相互運用性を確保することが開発者に求められる重要なスキルです。
本記事で紹介したベストプラクティスを参考に、最新のランタイム機能を最大限に活用した堅牢なアプリケーション開発を進めてください。
適切なモジュール管理を行うことで、コードの再利用性が高まり、チーム開発における混乱を未然に防ぐことができるでしょう。
Node.jsの進化は続いていますが、標準仕様であるESMを基軸に据えることで、将来的な技術変化にも柔軟に対応できるようになります。
