JavaScriptでモジュール方式にESM(ECMAScript Modules)とCommonJS(CJS)を選択する場面は増えています。過去から現在までのモジュール仕様の進化や特徴、最新ツールとの適合性を押さえておくことで、プロジェクトの設計やコード品質を高めることができます。この記事ではESM CommonJS 違いを中心に、それぞれの構造・実行時の挙動・互換性・移行のポイントなど、読者が「どっちを使うべきか」を見極められるように整理して解説します。
ESM CommonJS 違い:基本構造と歴史的背景
ESMとCommonJSの基本構造や歴史を理解することは、それらの違いを把握するための出発点になります。モジュールシステムとしていつ登場したか、どのような文法を持っているか、どの環境で使われてきたかを整理することで「ESM CommonJS 違い」がどのような意味を持つかが明確になります。
CommonJSの登場と役割
CommonJSはNode.jsで長く使われてきた標準的モジュール方式です。`require`による同期的なインポート、`module.exports`や`exports`を使ったエクスポートを特徴とします。サーバーサイドでのファイルシステムアクセスや同期処理を前提とする目的で設計されており、ブラウザではそのまま動かないためビルドやバンドラーによる変換が必要です。これらは、過去の互換性や既存コードベースの多さにより、現在でも広く採用されています。
ESMの誕生と標準化
ESMはECMAScript 2015(ES6)で標準仕様として導入されたモジュール方式です。文法としては`import`/`export`を用い、コードの静的解析が可能な構造が特徴です。ブラウザでもネイティブにサポートされ、Node.jsでも比較的新しいバージョンから対応が進み、最新環境では標準で利用できるようになっています。モダンな技術スタックではESMが推奨される理由です。
歴史的移行と互換性の背景
Node.jsは当初CommonJSのみをサポートしていましたが、開発やブラウザでの共通化のニーズに応える形でESMサポートが追加されました。過去には実験的なフラグを使って有効化する必要がありましたが、現在では主要なNode.jsバージョンでESMが正式な機能として使えます。TypeScriptなどの言語やバンドラー、ツールチェーンでもESM対応が標準となりつつあり、ESMとCommonJSの混在や移行が重要な課題になっています。
文法・構文の違いと動作特徴
モジュール方式として最も直接的に感じるのが文法や構文の違いです。どのキーワードを使い、どのタイミングでモジュールを読み込むか、トップレベルでawaitが使えるかなど、ESM CommonJS 違いの中心となる部分です。動作の違いが引き起こすエラーや制限もあるため、正しく使い分けることが重要です。
インポート・エクスポートの記法
CommonJSでは`require()`と`module.exports`や`exports`を用いてモジュールを読み書きします。`require`は実行時に評価され、文の途中にも設置可能です。一方でESMでは`import`と`export`を用い、静的構文としてファイルの先頭に置かれるケースが多く、実行前に依存関係が解決されます。この静的構造によりツールがモジュールグラフを解析しやすく、最適化やデッドコード削除などが効きやすくなります。
同期 vs 非同期読み込みとトップレベルawait
CommonJSの`require`は同期的にモジュールを読み込みます。モジュールの読み込みは呼び出し時点で即座に行われます。対してESMでは依存関係の解決フェーズが先行し、必要に応じて非同期の読み込みを伴うこともあります。さらに、ESMではトップレベルで`await`を使うことが許されていますが、CommonJSではこれが使えず、非同期処理を組み込むには回り道が必要です。
ファイル拡張子とpackage.jsonのtype設定
ESM CommonJS 違いはファイル拡張子やプロジェクトの設定にも現れます。ESMとして扱うためには`.mjs`拡張子を使うか、`package.json`で`”type”: “module”`と指定します。CommonJSはデフォルトで`.js`がこの方式、あるいは`”type”: “commonjs”`として明示できます。また、`.cjs`拡張子によりCommonJSとして明示できるファイルもあります。このような拡張子とtype指定は、モジュール解決時の挙動に大きな影響を与えます。
実行時の挙動とパフォーマンスの違い
文法だけでなく、モジュールが実行されるタイミングや性能にもESMとCommonJSの違いが表れます。依存関係の評価順、キャッシュの扱い、循環参照への対応などは、実際のアプリケーションでバグや性能ボトルネックになることがあります。ESM CommonJS 違いを理解することで、こうした問題を未然に防ぐことが可能です。
依存関係の解決と評価順序
CommonJSでは`require()`が実行された時点で依存モジュールを解決し、実行します。呼び出し順がそのままロード順となり、動的条件下で読み込むことができます。それに対してESMは静的にインポート宣言を先に解析し、全ての依存モジュールが解決された後でコードの実行が始まります。この構造によりモジュール全体の失敗が早く検出され、並列読み込みの利点も期待できます。
キャッシュとライブバインディング
CommonJSでは一度読み込んだモジュールはキャッシュされ、次回の`require`では同じモジュールを再利用しますが、エクスポートオブジェクトはコピーされることがありライブバインディングという概念はありません。これに対してESMではエクスポートされた値はライブバインディングされ、元の値の変更がインポート先に反映されます。キャッシュの作用も含めて、ESMはより柔軟でリアクティブな動作が可能です。
循環参照(circular dependency)への対応
CommonJSでは循環参照が発生した際に未処理のエクスポートがundefinedになることがあります。`require`による読み込み中に別モジュールを呼ぶと、完全に初期化されていない状態でのアクセスが生じ得ます。ESMではモジュール読み込みが静的であり、評価の段階で参照解決が管理されるため、循環参照が存在していても不完全な未定義アクセスが起こりにくく、より予測可能な挙動を示します。
ツール・互換性・エコシステムへの影響
実際の開発では言語やライブラリ、ビルドツールやTypeScriptとの互換性、既存モジュールとの統合が重要です。ESM CommonJS 違いはこれらの領域で特にインパクトが大きく、どの選択をするかによって開発コストや保守性が大きく変わります。
TypeScriptやバンドラーでのサポート状況
TypeScriptはESMおよびCommonJSの両方をemit可能であり、Node.jsの動作モデルに合わせたモジュール解決を行います。最新の設定では`–module node16`や`node18`などを指定すると、拡張子による判別やpackage.jsonのtype設定を正確に反映します。バンドラーもwebpackやRollupなどはESMを前提とした最適化(tree-shaking・コード分割など)に強く対応しており、ESMを使うほどメリットが得られます。
Node.js環境での互換性と制限
Node.jsではESMとCommonJSは混在可能ですが、制約があります。CommonJSからESMモジュールを同期的に読み込むことは古いバージョンではエラーを生じます。ESMモジュールはCommonJSモジュールをデフォルトインポートとして読み込むことはできますが、名前付きインポートは限定的であることがあります。これらの互換性差は最新版Node.jsでも考慮が必要です。
移行のベストプラクティスと注意点
既存のCommonJSコードベースをESMへ移行する場合、ファイル拡張子の扱い、package.jsonのtype設定、デフォルトエクスポート・名前付きエクスポートの使い分け、ビルドやツールチェーンとの整合性などを計画的に進める必要があります。文法変換だけでなく、依存モジュールがESM対応しているか、テスト環境が適切に動作するか、CI/CDやパッケージ公開の仕様に影響がないかなどを検証することが肝要です。
まとめ
ESMとCommonJSはどちらもJavaScriptにおけるモジュール方式ですが、文法・実行時挙動・互換性・エコシステム対応など多くの点で違いがあります。最新環境ではESMが標準的な選択肢であり、静的解析やブラウザ対応など多くの利点があります。一方で過去の資産や互換性の必要性からCommonJSが依然として役立つ場面もあります。
新規プロジェクトではESMを基本に選ぶことが望ましく、CommonJSは互換性確保や既存コード維持のために慎重に扱うことが大切です。ESM CommonJS 違いを正しく理解し、プロジェクトの要件に応じて使い分け・移行の計画を立てることで、コードの可読性・保守性・パフォーマンスを最大化できます。
コメント