1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

setTimeout はJavaScriptの機能ではない — エンジンとランタイムの境界をコードで確かめる

1
Last updated at Posted at 2026-08-12

この記事の対象読者

JavaScript は書けるけれど、ランタイムやエンジンの説明をしろと言われると詰まる人に向けた記事です。

  • 「Node.js は JavaScript のエンジンですか?」と聞かれて、うまく答えられない
  • ブラウザで動いたコードが Node.js で動かない理由がわかっていない
  • Vercel や Cloudflare の Edge Runtime で fsprocess が使えなくて戸惑った
  • setTimeoutfetch が JavaScript の機能なのか知らない

前提知識は JavaScript を書いたことがあることです。Node.js も触っていると読みやすいですが、必須ではありません。

逆に、V8 の中身(JITの最適化とかGCのアルゴリズムとか)を知りたい人には向きません。ここで扱うのは、どこまでがエンジンでどこからがランタイムか、という線引きの話だけです。


「Node.js は JavaScript のエンジンでしょうか?」

答えは No です。Node.js はランタイムで、エンジンは中に入っている V8 のことです。

とはいえ、その境界はどこなんでしょうか?setTimeout はどっち側なのか。Promise は?イベントループは?

わかりやすくするために、コードを例に説明してみます。Node.js には vm という標準モジュールがあって、ホストAPIが一切ない「素のV8コンテキスト」を作れます。そこで何が使えて何が使えないかを見れば、その境界が見えてくるかもしれません。

検証環境: Node.js v20.11.1 / macOS

まず境界を見てみる

vm.createContext({}) に空オブジェクトを渡すと、何も注入されていないコンテキストが手に入ります。あとはそこで typeof を並べるだけ。

const vm = require("node:vm");
const ctx = vm.createContext({});   // ホストAPIを一切入れない

const names = [
  "Promise", "JSON", "Math", "Symbol", "Map", "Proxy", "Reflect", "Intl",
  "setTimeout", "setInterval", "queueMicrotask", "fetch", "process",
  "Buffer", "URL", "TextEncoder", "structuredClone", "AbortController",
];

for (const n of names) {
  console.log(n.padEnd(18), vm.runInContext(`typeof ${n}`, ctx));
}

結果です。

Promise            function
JSON               object
Math               object
Symbol             function
Map                function
Proxy              function
Reflect            object
Intl               object
setTimeout         undefined
setInterval        undefined
queueMicrotask     undefined
fetch              undefined
process            undefined
Buffer             undefined
URL                undefined
TextEncoder        undefined
structuredClone    undefined
AbortController    undefined

きれいに二分されました。

上半分(PromiseJSONMathSymbolMapProxyReflectIntl)は素のコンテキストにもあります。ECMAScript 仕様(ECMA-262)が定義しているもの、つまりエンジンが持っている部分です。

下半分は全部 undefinedsetTimeoutfetchprocess も、ECMAScript の仕様には一行も書かれていません。普段使えているのは、ホスト環境が足してくれているからです。

呼んでみると、そもそも存在しないと怒られます。

vm.runInContext("setTimeout(() => {}, 0)", ctx);
// ReferenceError: setTimeout is not defined

素のコンテキストのグローバルは、数えたら62個しかありませんでした。普段書いている JavaScript は、思っているよりホスト環境に依存しています。

エンジンとランタイムの役割分担

図にするとこう。

エンジン ランタイム
準拠する仕様 ECMAScript(ECMA-262) HTML仕様、各ランタイムの独自仕様
主な仕事 パース、JITコンパイル、実行、GC イベントループ、タイマー、I/O、モジュール解決
提供するもの Object Array Promise JSON Math setTimeout fetch process window
V8、JavaScriptCore、SpiderMonkey、Hermes Node.js、Deno、Bun、ブラウザ、Cloudflare Workers

エンジンがやるのは、JavaScript という言語を実行することだけ。ファイルを読むことも、通信することも、時間を測ることもできません。全部ランタイム側の仕事です。

組み合わせは1対1ではない

エンジンとランタイムは別々に選べます。以下はその表です。

ランタイム エンジン
Node.js V8
Deno V8
Bun JavaScriptCore
Chrome / Edge V8
Firefox SpiderMonkey
Safari JavaScriptCore
Cloudflare Workers(workerd) V8
React Native Hermes

ここで目を引くところが2つあります。

まず、Node.js と Chrome は同じ V8 なのに書けるコードが違う。document は Chrome にあって Node にない。process はその逆です。エンジンが同じでも、ホストが違えば使えるAPIは変わります。

もうひとつ、Bun だけエンジンが違います。Bun は起動が速いとよく言われますが、理由のひとつはこれです。JavaScriptCore は起動時間とメモリ使用量に強い。ランタイムを Zig で書いたから速い、という話だけではありません。

イベントループはどちらにあるか

イベントループはエンジンにありません。

V8 にそれは含まれていません。Node.js のイベントループは libuv というライブラリが持っており、ブラウザのそれは HTML仕様が定義しています。同じ V8 を積んでいても、この部分は完全に別物です。

じゃあ非同期処理は全部ランタイム側かというと、そうでもない。Promise のジョブキューは ECMAScript 仕様の側にあります。

これも確かめられます。ホストAPIが何もないコンテキストで Promise.then が動くかどうか。

const vm = require("node:vm");

const log = [];
const ctx = vm.createContext({ report: (s) => log.push(s) });

vm.runInContext(`
  report("sync-1");
  Promise.resolve().then(() => report("microtask"));
  report("sync-2");
`, ctx);

console.log("実行直後:", JSON.stringify(log));
setTimeout(() => console.log("次のティック:", JSON.stringify(log)), 0);
実行直後: ["sync-1","sync-2"]
次のティック: ["sync-1","sync-2","microtask"]

setTimeout すら存在しないコンテキストで、Promise.then は動きました。

しかも実行されたのは vm.runInContext() が返ってきた後です。ホスト側に制御が戻ってから動いている。

分担はこうなっているようです。

  • ジョブキューの存在と、コールバックが積まれる順序 → ECMAScript 仕様(エンジン)
  • キューをいつ流すか → ホスト(ランタイム)

だから Promise.then 同士の実行順はどの環境でも同じ。でも setTimeout との前後関係はホスト次第です。マイクロタスクとマクロタスクの話が環境ごとに微妙に食い違って見えるのは、たぶんこの構造のせいです。

境界はきれいに割り切れない

ここまで「エンジンはECMAScript、それ以外はホスト」で通してきましたが、例外があります。console です。

さっきの検証で、console は素のコンテキストにも存在していました。ECMAScript の仕様に console はないのに。

正体を調べます。

const vm = require("node:vm");
const ctx = vm.createContext({});

console.log("Nodeのconsoleと同一か:", vm.runInContext("console", ctx) === console);
vm.runInContext(`console.log("vm内から出力")`, ctx);
Nodeのconsoleと同一か: false

別物でした。しかも console.log を呼んでも何も出ません("vm内から出力" はどこにも現れませんでした)。

おそらく V8 がデバッグ用(インスペクタ)に持っている console で、ECMAScript 仕様のものではないはずです。ここは仕様まで追いきれていないので断言は避けますが、出力先をホストが繋いでいないから何も起きない、という理解で辻褄は合います。普段ターミナルに文字が出るのは、Node.js が出力先を用意してくれているからです。

「エンジンは言語機能だけ」は原則としては正しいです。ただ実際のエンジンは、こういうデバッグ支援みたいな付随機能も抱えています。きれいに二分できるわけではなく、原則と例外があるようです。

これが分かると何が説明できるか

境界が見えると、実務で困る場面に説明がつきます。

ブラウザ用のコードが Node.js で動かない。
windowdocumentlocalStorage も、エンジンではなくブラウザというホストの持ち物です。実際、Node.js 本体のグローバルにもこれらはありませんでした(typeof windowundefined)。同じ V8 でも、ホストが違えば使えない。

Edge Runtime(Vercel、Cloudflare Workers)で Node.js の API が使えない。
Cloudflare Workers はエンジンこそ V8 ですが、ランタイムは workerd という別物。fsprocess も Node.js が提供しているものなので、ホストが変われば消えます。「同じ JavaScript なのになぜ」と思うところですが、変わったのはエンジンではなくランタイムのほうです。

Node.js のバージョンを上げると新しい構文が使える。
構文を実装しているのはエンジン側です。Node.js を上げると同梱の V8 も上がるので、使える構文が増えます。逆に fetch のようなAPIが増えるのは Node.js の仕事なので、こちらはエンジンとは無関係に足されていきます。

Babel とポリフィルの守備範囲が違う。
Babel は新しい構文を古い構文に書き換えるツールで、これはエンジン側への対処。core-js のようなポリフィルは、存在しないAPIを実装して足すものです。構文が足りないのかAPIが足りないのかで打つ手が変わるのは、この境界があるからです。

まとめ

  • setTimeoutfetchprocess も ECMAScript の仕様にはない。ホスト環境が足しているもの
  • 素のV8コンテキストのグローバルは62個だけ。PromiseJSON はあるが、setTimeout を呼ぶと ReferenceError
  • エンジンは ECMAScript の実装(パース、JIT、実行、GC)。ファイルも通信も時間も扱えない
  • ランタイムはエンジン+ホストAPI(イベントループ、タイマー、I/O)
  • 組み合わせは1対1ではない。Node と Chrome は同じ V8、Bun だけ JavaScriptCore
  • イベントループはランタイム側(Nodeはlibuv、ブラウザはHTML仕様)。ただし Promise のジョブキューは ECMAScript 側で、いつ流すかだけホストが決める
  • 境界は完全には割り切れない。console はV8が持っているのに仕様外で、ホストが繋がないと何も出ない

「同じ JavaScript なのに動かない」に出くわしたら、まず足りないのがエンジンの機能なのかホストのAPIなのかを切り分けると良いかもしれません。

参考

1
0
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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?