LoginSignup
140
78

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-10-19

この投稿では、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しているコードで具体的に考えてみましょう:

// 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

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

7esdbky214p0ke717qgumowjdk6y.png

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

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

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

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

// 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

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

post-order traversalの探索順序

i5smrfy4wyi8ejxh02zkbkrwvsd6.gif

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

e3w5k8bb6tiubfh7uouefeyndp42.png

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

2k1scf9lrn23iea41pybg52n08k1.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

140
78
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
140
78