Menu
日本語

CommonJS vs ES Modules|requireとimportの違い

Node.jsにはなぜ2つのモジュール方式があるのか。requireとimportの違い、使い分けの基準、.mjs/.cjsの扱いまでまとめて整理します。

2つのモジュールシステム、1つの言語

そもそもJavaScriptには、モジュールという仕組みがありませんでした。その穴を埋めるかたちで2009年にNodeが導入したのが CommonJS(requiremodule.exports)で、長らくNodeのコードといえばこの書き方が定番でした。その後、2015年になって言語自体に標準のモジュールシステム — ES Modules(importexport) — が追加され、今ではブラウザでもNodeでも使えるようになっています。

そんなわけで、実際のコードでは両方を目にすることになります。まったく同じ小さなモジュールを、それぞれの書き方で並べてみましょう。

index.js
Output
Click Run to see the output here.
index.js
Output
Click Run to see the output here.

同じ機能でも、包み紙が違うだけ。このページの残りでは、どちらの包み紙がいつ効いてくるのか、そしてどう使い分けるかを見ていきます。

構文の違い

普段使いでの違いは、ハガキ1枚にまとまる程度です。

// CommonJS
const fs = require("fs");
const { readFile } = require("fs/promises");

module.exports = something;
module.exports.name = value;
exports.name = value;
// ES Modules
import fs from "fs";
import { readFile } from "fs/promises";

export default something;
export const name = value;
export { name };

require はただの関数呼び出しですが、import は文(ステートメント)です。モジュールのトップレベルにしか書けませんし、パスも文字列リテラルじゃないといけません。この制約、別に意地悪で付いてるわけじゃなくて、これがあるおかげで ESM は CommonJS にできない芸当ができるんです。

本質的な違いは「静的か動的か」

CommonJS の require() は、その行が実行されたタイミングで評価されます。なので if の中に入れたり、パスを実行時に組み立てたり、条件付きでモジュールを読み込んだり、そういうことが自由にできます。

index.js
Output
Click Run to see the output here.

ES Modulesは静的な仕組みです。エンジンはコードを実行する前にすべてのimport文を解析し、依存関係グラフを構築して、事前にすべてを解決します。パスがリテラル文字列でなければならないのも、importがトップレベルにしか書けないのも、このためです。

この設計のメリットは大きく、ツールがコードを実行しなくてもモジュールグラフ全体を把握できる点にあります。バンドラーがtree-shaking(使われていないexportを削除する処理)を行えるのも、エディタが正確な補完を出せるのも、ブラウザがモジュールを並列で取得できるのも、すべてこの静的解析のおかげです。

ESMで本当に動的な読み込みが必要な場面では、import()を使います。これは関数のように呼び出せる式で、Promiseを返します。

index.js
Output
Click Run to see the output here.

Node がファイルのモジュール形式をどう判定するか

ひとつの Node プロジェクトのなかで、CommonJS と ES Modules を混在させることもできます。Node は以下の 2 つを手がかりに、各ファイルがどちらの形式かを判断します。

  • ファイルの拡張子: .mjs なら必ず ESM、.cjs なら必ず CommonJS になります。
  • 一番近い package.json"type" フィールド: "module" が指定されていれば .js ファイルは ESM として扱われ、"commonjs"(未指定のときのデフォルト)なら CJS として扱われます。
// package.json
{
    "name": "my-app",
    "type": "module"
}

"type": "module" が設定されている場合、同じパッケージ内の普通の hello.jsimport/export を使うことになります。そこに hello.cjs を一つ置けば、そのファイルだけは require を使うという具合です。この仕組みのおかげで、プロジェクトを少しずつ移行したり、ライブラリで両方の形式を同梱したりできるわけです。

初心者がハマりやすいポイント: ESM ファイルの中では requiremodule.exports も存在しません。手癖で書いてしまうと ReferenceError が飛んできます。

ESM と CommonJS の相互運用

ESM ファイルから CommonJS のパッケージを読み込みたい、あるいはその逆をやりたい、という場面はよくあります。ただし、両者のルールは対称ではありません。

ESM から CommonJS を import するのは素直にできます。CJS 側の module.exports オブジェクトが、そのまま default export として扱われます:

index.js
Output
Click Run to see the output here.
// app.mjs
import greet from "./greet.cjs";
console.log(greet("Rosa"));

CommonJS からの名前付きインポートは動くこともあります(Node が名前付きエクスポートを静的に検出しようとするため)が、確実に動かしたいなら default をまるごと受け取って分割代入するのが安全です。

import pkg from "./utils.cjs";
const { parse, stringify } = pkg;

CommonJSからESMを読み込むパターンが一番厄介です。ES モジュールを require() しようとすると ERR_REQUIRE_ESM が投げられて失敗します。逃げ道になるのが動的 import() で、これは CJS 側でも使えて Promise を返してくれます。

index.js
Output
Click Run to see the output here.

新しめの Node.js(22 以降)では、条件付きで ESM を同期的に require() できるようになりました。ただし、どの環境でも確実に動くのは動的 import() です。

その他、押さえておきたい挙動の違い

構文以外にも、CommonJS と ES Modules では細かい仕様が食い違う部分があり、ときどきハマる原因になります。

index.js
Output
Click Run to see the output here.

さらにいくつか注意点を挙げておきます。

  • トップレベルの this CJS ではトップレベルの thismodule.exports を指しますが、ESM では undefined です。ESM は常に strict モードで動きます。
  • __dirname__filename CJS ならそのまま使えますが、ESM では import.meta.url から自分で組み立てる必要があります。
import { fileURLToPath } from "node:url";
import { dirname } from "node:path";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
  • import 時の拡張子。ESM では相対パスの拡張子が必須です("./utils" ではなく "./utils.js")。CJS はその辺りがゆるくても動きます。
  • ライブバインディング vs スナップショット。ESM の import はエクスポート元の変数への生きた参照ですが、CJS は読み込み時点で module.exports に代入されていた値のコピーを返します。普段は気にする場面がほぼありませんが、循環依存があるときに効いてきます。

CommonJS と ES Modules、どっちを使う?

新規プロジェクトなら迷わず ES Modules です。package.json"type": "module" を書いたら、あとは振り返らなくて大丈夫。ESM は言語仕様そのものですし、ブラウザでも Node でも同じように動き、トップレベル await も使えて、ツール側も ESM 前提で作られるようになっています。

CommonJS を選ぶのはこんなケースです。

  • 既存の CJS コードベースを保守していて、まだ移行コストに見合わないとき。
  • かなり古い Node バージョンや、ESM を使えない利用者もサポートしたいライブラリを公開するとき。
  • 重要な依存パッケージが CJS 専用で、相互運用が面倒くさいとき(最近はかなり減りましたが、ゼロではありません)。

とはいえ、そんな場合でも ESM のコードを読む機会はどんどん増えています。ここ数年に npm に公開されたパッケージは ESM 寄りのものばかりだからです。両方読めることはもはや前提で、そのうえで自分が実際に書いているほうのイディオムにしっかり慣れておくのが大事です。

サッと確認できるチェックリスト

新しいファイルを開いたとき、まずこう自問してみてください。

  • このファイルは import/export 派? それとも require/module.exports 派? 混在させないこと。
  • 一番近い package.json"type" はどうなっている?
  • パッケージを import するなら、そのパッケージの package.json も確認。ESM 配布? CJS 配布? それとも両対応?
  • ERR_REQUIRE_ESM が出たら、CJS から ESM を読み込もうとしているサイン。動的 import() に切り替えるか、呼び出し側を ESM に寄せましょう。

Node でハマるモジュール周りの混乱の 9 割は、この 4 つのどれかです。

次は npm の基本

モジュールは 自分の コードをファイル単位に分けるための仕組みでした。次のステップは、他の人が書いたコードを取り込むこと。そこで登場するのが npm です。パッケージのインストール方法、semver のバージョン範囲の読み方、それから日々の開発で実際に使う npm のワークフローをひと通り押さえていきます。

よくある質問

JavaScriptのrequireとimportは何が違う?

require はCommonJS方式のモジュール読み込みで、同期的に、呼ばれたその場で実行され、module.exports に代入された値をそのまま返します。一方の import はES Modulesの構文で、静的かつファイル先頭にホイストされ、コード実行前に解析されます。そのほか、this の中身、循環参照の解決方法、トップレベル await が使えるかどうかといった点でも挙動が違います。

新しいNodeプロジェクトではCommonJSとES Modulesどちらを使うべき?

基本はES Modulesでいきましょう。package.json"type": "module" を指定して、import/export で書けばOKです。ESMは公式標準で、ブラウザとNodeの両方で動き、トップレベル await もそのまま使えます。ただし古いパッケージやツール周りには今もCommonJSが残っているので、自分では書かなくても読む機会はあります。

同じプロジェクトでrequireとimportを混在させてもいい?

ルールを守れば可能です。.mjs ファイルや "type": "module" のパッケージはESM、.cjs"type": "commonjs" はCJSとして扱われます。ESM側からCommonJSを import するのはOKで、その際 module.exports はデフォルトエクスポートとして受け取れます。逆にCommonJSから直接 require() でESMを読み込むことはできず、Promiseを返す動的 import() を使う必要があります。

Coddyでコードを学ぼう

始める