🚀 ニフティ’s Notion

【Webアプリ2025 #22】JavaScriptの諸事情1: ランタイムとモジュールシステム

JavaScriptランタイム

動作にあたり動作環境(ランタイム)を必要とするプログラミング言語には、たいてい「標準」といえるランタイムが存在する。

ex)

  • Python: CPython
  • Java: Oracle JDK

ところがJavaScriptのランタイムは複数あり、「標準」がない。

ブラウザ

元々JavaScriptはブラウザで実行するものであり、各ブラウザが独自のJavaScriptエンジン(ランタイムのコア)を実装している。

  • Chrome(Chromium): V8
    • Edge、OperaなどはカスタムChromiumなのでここ
  • Firefox: SpiderMonkey
  • Safari: JavaScriptCore

これらはそれぞれ挙動が異なり、対応するJavaScriptの文法も違う。さらにブラウザバージョンによっても違う。

ローカル・サーバ向けランタイム

JavaScriptをサーバでも使いたいということで、ブラウザ外で動作するJavaScriptランタイムも作られるようになった。

後にフロントエンド開発全般において使用されるようになる。

Node.js

ローカル・サーバ用として初めて実装されたランタイムであり、デファクトスタンダード。ChromiumのJavaScriptエンジンであるV8を内部で使用している。

開発用途においては、特別な理由や思想がない場合はこれを使えばよいはず。

Deno

Node.jsの作者(ライアン・ダール)が新しく作成したランタイム。同様にV8を使用している。

Node.jsに対する作者の反省を元に作られており、Node.jsとは異なるパッケージシステムなどを採用する。

bun

高速性を売りにしたランタイム。

Node.js/Denoとは異なり、Safariで使われるJavaScriptCoreをJavaScriptエンジンとして採用している。

💡
ほとんどのフレームワーク・ライブラリはNode.jsを前提として作られているため、Node.jsだけ覚えておけば問題はないです
エッジランタイム

最近はCDNに設置されたエッジで動作するランタイムも出現している。

HTTPリクエストごとに起動するサーバレスであることがほとんどで、実行時間やメモリ量、アプリケーションサイズ、使用できる文法などに大きな制約がかかる。

近年は制約がゆるくなってきたこともあり、エッジランタイムでNext.jsなどのフレームワークを動かしてしまう例も増えてきている。

Cloudflare Workers
実行時間 ~30秒
メモリ ~128MB
バンドルサイズ ~10MB
(1MBまでを推奨)

Cloudflare社が提供するCDN上で動作するエッジランタイム。

Node.jsと同じくV8をベースにしているため、比較的Node.jsとの互換性が高い。

Lambda@Edge
実行時間 ~5秒
メモリ ~128MB
バンドルサイズ ~50MB

AWS Lambdaのエッジ版。CloudFront上で動作する。

基本的にはLambda同様にNode.jsを利用できるが、環境変数が使えないなどの制限がかかる。実行時間の制限なども通常のLambdaより厳しい。

CloudFront Functions
実行時間 1ミリ秒未満
メモリ ~2MB
バンドルサイズ ~10KB

同じくCloudFront上で動くエッジランタイム。

動作条件が非常に厳しく、またNode.jsではなく独自ランタイムになっている。JavaScript文法も非常に限られたものしか使えない。

その代わりLambda@Edgeより高速・大量・安価にアクセスをさばける。

言語仕様

JavaScriptは各ランタイムが独自に実装しているため、その動作はバラバラである。

一応、 文法は ECMAScript という形で規格化されている。

2015年以降は毎年仕様が更新され、ECMAScript2024のような形で呼ばれる。

ただし各ランタイムによって対応状況はまちまちで、個別の文法レベルで対応状況が異なる。

文法ごとの対応状況はCanIUseなどのサイトで確認することになる…

モジュールシステム

他言語でいう import など、他のソースファイルやライブラリを読み込む機能をモジュールシステムと呼ぶ。

JavaScriptにはもともとモジュールシステムは存在しなかった。元はHTMLからしか呼び出せなかったので、

<head>
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
  <script>
    /* ここに自前のJavaScriptソース */
  </script>
</head>

のようにすればライブラリは読み込めたからである。

Node.jsなどの出現により、HTMLを経由することなくJavaScriptから別のJavaScriptファイルを読み込む必要が生じた。ここで歴史的経緯により複数のモジュールシステムが存在してしまっている。

CommonJS (CJS)
// ライブラリ側
const add = (a: number, b: number): number => {
  return a + b;
};

module.exports = {
  add
};
lib.js
// 呼び出す側
const { add } = require('./lib');

add(1, 2);
app.js

Node.jsが最初に採用したモジュールシステム。 require() を用いて別ファイルを読み込む。

それまでのJavaScript文法を壊さないように導入されたので、 require() はただの 関数 である。JavaScriptの文法ではない。

ES Modules (ESM)
// ライブラリ側
const add = (a: number, b: number): number => {
  return a + b;
};

export { add };
lib.js
// 呼び出す側
import { add } from './lib.js';

add(1, 2);
app.js

CommonJSなどの出現を受け、JavaScript(ECMAScript)の 文法 として整備されたモジュールシステム。ECMAScript 2015より使用できる。

公式な文法なのでブラウザでも使える。Node.js 12以上でもサポートするようになった。

Node.js界隈でもこちらへの移行が進んでおり、いずれは統一されるものと思われる。

既に書いてしまったコードはなかなか移行が難しく、各社苦労している。