Node.js: require()は同期型ロード、importは非同期型ロード

この投稿では、Node.jsにおいてCommonJSとES Modulesのモジュールロードの仕組みの違いを、同期的か非同期的かの観点で説明します。

本稿では、CommonJSはCJS、ES ModulesはESMと略称で記載します。

:bow: 執筆にあたり、できるだけ努力して調査したつもりですが、もし用語の使い間違いや誤った説明などがございましたら、ご指摘や訂正の提案などを頂けると幸いです。

## 同期的なCJS、非同期的なESM

CJSはもともとサーバサイドJavaScriptにおけるモジュールの問題を解決するためにねられた仕様でした。サーバサイドではJSファイルがローカルディスク上にあることが普通です。そのため、JSファイルを探し、ファイルの内容を読み込む処理は**CJSでは同期的なロード方式**になっています。

一方の、ESMはサーバサイドだけでなく、ブラウザで使われることも考えて、同期的なロード方法に限定しないことになっています。もしも、同期的なロードに限定してしまうと、ブラウザでのユーザ体験が悪くなるからです。

ESMのロード方式を同期的な実装にするか、非同期的にするかは、JavaScriptの実行エンジンが決めていいことになっています。**Node.jsは、ESMは非同期的にロードする方式を採用**しています。

## ファイル読み込みと実行

モジュールのロードプロセスは基本的に、JavaScriptのファイルを読み込み実行されるわけですが、ファイル読み込みと実行のタイミングはCJSとESMで異なってきます。

### CJSのファイル読み込みと実行

CJSでは、モジュールを`require`したとき、モジュールごとに「ファイル読み込み」→「実行」の手続きをセットで行っていきます。

例えば、次の例のように3つのモジュールを`require`しているコードで具体的に考えてみましょう:

```js
// 1.js
console.log("1.js");

// 2.js
console.log("2.js");

// 3.js
console.log("3.js");

// cjs.js
require("./1");
require("./2");
require("./3");
```

このcjs.jsを動かすと、ロードプロセスはどんな流れで進んでいくでしょうか。まず、1行目で`require`した1.jsのファイルの内容が読み込まれます。そして、そのJavaScriptコードが評価実行されます。ここで、コンソールの標準出力に`1.js\n`が出力されます。次に、2.jsが読み込まれ、実行、`2.js\n`が出力されます。最後に、3.jsが読み込まれ、実行、`3.js\n`が出力されます。

![mjqr35538ahk8o6v8p5niscfj7nc.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889/e6063282-79fe-4e02-32e3-0ec01eead741.png)


ちなみに、CJSのロードプロセスが実際にどうなっているかは、環境変数`NODE_DEBUG=module`をセットしてNode.jsを実行してみると観察することができます。

![7esdbky214p0ke717qgumowjdk6y.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889/e17fc320-610d-069b-1ee3-d4b1616a617f.png)


CJSが同期的と言われるのは、このようにモジュールのファイル読み込みと実行が同時期に発生しながらロードプロセスが進んでいくためです。

### ESMのファイル読み込みと実行

Node.jsのESMでは、モジュールを`import`したとき、非同期的なロードを行うようになっています。なぜ非同期かというと、モジュールのファイル読み込み、依存関係グラフの構築、そして、実行がバラバラのタイミングで行われるためです。

具体的に、次のように3つのモジュールを`import`しているコードを見てみましょう:

```js
// 1.mjs
console.log("1.mjs");

// 2.mjs
console.log("2.mjs");

// 3.mjs
console.log("3.mjs");

// esm.mjs
import "./1.mjs";
import "./2.mjs";
import "./3.mjs";
```

このesm.mjsを動かすと、モジュールのロードプロセスは次のようになります。まず、esm.mjs自体が読み込まれ、静的解析され、依存するモジュールのリストが作られます。この時点では、esm.mjs自体はまだ実行されません。

1.mjs、2.mjs、3.mjsがリストアップされるので、このリストに基づいて、3つファイルを並行で読み込みます。これらも静的解析され、依存するモジュールのリストを作るのですが、これ以上依存先がないため、モジュールの依存関係グラフが完成します。

![5mj3nwvxnh2wbgyhbaakks64mdcl.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889/ffba2726-3839-9e26-e8bf-2f83baf0e30a.png)


最後にそのグラフに基づいて、各モジュールを実行していきます。実行順は、post-order traversalというアルゴリズムで行われます。ざっくりいうと、グラフの中で末端かつ左側に位置するモジュールから実行していき、ルートに到達するまでそれを繰り返すというものです。

_post-order traversalの探索順序_

![i5smrfy4wyi8ejxh02zkbkrwvsd6.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889/3b285590-6c73-a266-14d2-c4ce5e1ad40a.gif)

なので、この例では、まず1.mjsが実行され、コンソールの標準出力に`1.mjs\n`が出力されます。次に、2.mjsが実行され、`2.mjs\n`が出力されます。最後に、3.mjsが実行され、`3.mjs\n`が出力されます。そして最後に`esm.mjs`が実行されます。

![e3w5k8bb6tiubfh7uouefeyndp42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889/38a597df-ff5e-50c9-f24b-4d2866f5dad8.png)


ちなみに、ESMのロードプロセスが実際にどうなっているかは、環境変数`NODE_DEBUG=esm`をセットしてNode.jsを実行してみると観察することができます。

![2k1scf9lrn23iea41pybg52n08k1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889/3a4e13ce-a1f8-608d-83f7-bbba29fe9b9b.png)

## ESMローダーが非同期型であることのメリット

ESMのロード方式は、ECMAScriptの仕様で決められておらず、Node.jsが非同期的ロードなのはNode.jsがそう作ったからというのは前述したとおりです。Node.jsが同期的なローダーを実装する選択肢も無くはなかったわけですが、非同期を採用したことでいくつかメリットも生まれました。

### メリット1: 並行読み込みによるパフォーマンス

CJSは同期的ロードなので、モジュールをロードし終わるまでは、別のモジュールをロードすることができません。ESMは非同期的なので、複数のモジュールを並行して読み込むことができます。このおかげで、読み込むモジュールが多くなるほど、パフォーマンスにいい影響を与えられると考えられます。

### メリット2: リモートモジュール対応への将来性がある

Node.jsはブラウザと異なり、`import "https://..."`のようにして、リモートのモジュールをロードすることは現在できません。しかし、ESMローダーが非同期な設計になっているおかげで、リモートのモジュールをダウンロードしてきて実行することが、技術的には可能になっています。もしも、将来的にNode.jsがリモートモジュールに対応した場合、ローダーの仕様を変えないで済むので、Nodeユーザとしては下位互換性を崩されることなく、リモートモジュール対応の恩恵を享受できることでしょう。

### メリット3: ブラウザのローダーと同じ

ブラウザはリモートモジュールをロードする必要があるので、非同期的なローダーになっています。Node.jsもブラウザと同じ非同期型であることはメリットです。モジュールを開発するとき、「ブラウザではこう、Node.jsではこう」と場合分けしてモジュールの設計を考えずに済みます。また、モジュールを使う側としても、同じマインドセットで臨めるので、学習コストがかからなかったり、知らぬがゆえにバグらせてしまうといった事故も避けられます。

---

最後までお読みくださりありがとうございました。Twitterでは、Qiitaに書かない技術ネタなどもツイートしているので、よかったらフォローお願いします:relieved:→[Twitter@suin](https://twitter.com/suin)