Skip to content

JavaScript ランタイム深掘りガイド

はじめに

JavaScript の基本文法はもう学びましたが、次のような疑問を持ったことはありませんか:

  • コードは一体どこで実行されているのか?
  • なぜ同じコードがブラウザと Node.js で異なる動作をするのか?
  • なぜコードが時には「フリーズ」し、時には「並列」に実行されるように見えるのか?

この記事では、イベントループ、コールスタック、メモリ管理などを含む JavaScript のランタイム環境について深く掘り下げます。読み終えると、コードがなぜ特定の順序で実行されるのかを理解し、非同期関連のバグを迅速に特定し、コードのパフォーマンスを最適化し、メモリリークを回避できるようになります。

この記事で学ぶこと

内容学べばできること
第 1 章ランタイムの概要JavaScript コードがどこで実行されるかを理解する
第 2 章ブラウザランタイムブラウザがどのような Web API を提供しているかを知る
第 3 章Node.js ランタイムサーバー側の JavaScript 環境を理解する
第 4 章イベントループの深掘りマクロタスクとマイクロタスクの実行順序を把握する
第 5 章コールスタックとメモリコードの実行プロセスとメモリ管理を理解する
第 6 章実践的なテクニックパフォーマンスの最適化、メモリリークのデバッグ

1. ランタイムの概要

🤔 核心の問い

「ランタイム」とは何か? JavaScript は単なる言語にすぎないのに、なぜ同じコードが異なる環境で異なる動作をするのか?

1.1 ランタイムとは何か

ランタイム = JavaScript エンジン + 環境が提供する API

JavaScript を「プログラミング言語」に例えるなら、ランタイムは「オペレーティングシステム」です——コードが何をでき、何ができないかを決定します。

┌─────────────────────────────────────┐
│         JavaScript コード             │
├─────────────────────────────────────┤
│      JavaScript エンジン (V8)         │  ← コードの解析と実行を担当
├─────────────────────────────────────┤
│      ランタイム環境 (ブラウザ/Node.js)  │  ← 追加の機能を提供
└─────────────────────────────────────┘

一つの例え:JavaScript は「共通語」、ランタイムは「都市」

  • JavaScript の文法(共通語)はどこでも同じ
  • しかし、異なる都市が提供する施設は異なる:
    • ブラウザ = DOM、window、fetch がある(商業施設や図書館がある都市のようなもの)
    • Node.js = fs、http、path がある(工場や高速道路がある都市のようなもの)

1.2 二つの主要なランタイム

特徴ブラウザNode.js
主な用途ウェブインタラクション、ユーザーインターフェースサーバー側アプリケーション、CLI ツール
グローバルオブジェクトwindowglobal
DOM API✅ 対応❌ 非対応
ファイルシステム❌ 制限あり✅ 完全対応
モジュールシステムES ModulesCommonJS + ES Modules
タイマーsetTimeout, setIntervalsetTimeout, setInterval
ネットワークリクエストfetch, XMLHttpRequesthttp, https モジュール

👇 試してみましょう:ブラウザと Node.js の環境の違いを比較する

运行时环境对比

浏览器环境

window
浏览器全局对象
window.location.href
document
DOM 操作
document.querySelector("h1")
localStorage
本地存储
localStorage.setItem("key", "value")
fetch
网络请求
fetch("/api/data")
setTimeout
定时器
setTimeout(() => {}, 1000)
特点:
  • ✅ 有 DOM 和 BOM API,可以操作网页
  • ✅ 有 Web Storage (localStorage, sessionStorage)
  • ✅ 有 fetch 和 XMLHttpRequest 进行网络请求
  • ❌ 没有文件系统访问权限
  • ❌ 不能直接创建 HTTP 服务器

代码演示:不同环境的差异

🌐浏览器结果
点击"在浏览器运行"查看结果
🟢Node.js 结果
需要在 Node.js 环境中运行

核心区别:

浏览器运行时专注于用户界面和网页交互,提供 DOM、BOM、fetch 等前端专用 API。

Node.js 运行时专注于服务器端开发,提供文件系统、HTTP 服务器、进程管理等后端专用 API。

同样的 JavaScript 语法,但能用的 API 完全不同——这就是"环境判断"的重要性。

💡 核心の気づき

ランタイムが使用できる API を決定します。ブラウザで使える DOM API は Node.js では使えませんし、Node.js で使えるファイル API はブラウザでは使えません。これが一部のコードで「環境判定」が必要な理由です。


2. ブラウザランタイム

🤔 核心の問い

ブラウザは JavaScript がウェブページを操作するためにどのような機能を提供しているのか?

2.1 ブラウザランタイムの構成

┌─────────────────────────────────────────────┐
│            JavaScript エンジン               │
│            (V8 / SpiderMonkey)              │
└─────────────────────────────────────────────┘

┌─────────────────────────────────────────────┐
│              Web APIs                        │
│  ┌─────────┐ ┌──────────┐ ┌──────────┐     │
│  │   DOM   │ │   BOM    │ │ Network  │     │
│  │ページ操作│ │ブラウザ操作│ │ネット通信 │     │
│  └─────────┘ └──────────┘ └──────────┘     │
└─────────────────────────────────────────────┘

┌─────────────────────────────────────────────┐
│           イベントループ (Event Loop)         │
│     コードの実行、イベント処理、タスクスケジュール│
│     を調整                                    │
└─────────────────────────────────────────────┘

2.2 Web API の三つのカテゴリ

1. DOM API - ページの内容を操作

javascript
// 要素の検索
const title = document.querySelector('h1')

// 内容の変更
title.textContent = '新しいタイトル'

// スタイルの追加
title.style.color = 'red'

2. BOM API - ブラウザを操作

javascript
// ページ遷移
window.location.href = 'https://example.com'

// ブラウザストレージ
localStorage.setItem('key', 'value')

// ブラウザ履歴
history.back()

3. Network API - ネットワークリクエスト

javascript
// HTTP リクエストの送信
fetch('/api/data')
  .then(response => response.json())
  .then(data => console.log(data))

2.3 ブラウザ特有のイベントメカニズム

ブラウザランタイムの最も強力な機能の一つは「イベント駆動」です——コードは常に実行され続けるのではなく、ユーザーの操作があったときに初めて実行されます。

javascript
button.addEventListener('click', () => {
  console.log('ボタンがクリックされました')
})

一般的なイベントの種類:

イベントの種類発生のタイミング実際のシーン
clickマウスクリックボタンのインタラクション
input入力フィールドの内容変更リアルタイム検索
scrollページのスクロール遅延読み込み
loadリソースの読み込み完了データの初期化
errorエラーの発生エラー処理

3. Node.js ランタイム

🤔 核心の問い

JavaScript がサーバー側で実行できるのは何のおかげか?

3.1 Node.js の構成

┌─────────────────────────────────────────────┐
│            JavaScript エンジン               │
│                 (V8)                        │
└─────────────────────────────────────────────┘

┌─────────────────────────────────────────────┐
│           Node.js 組み込みモジュール           │
│  ┌─────────┐ ┌──────────┐ ┌──────────┐     │
│  │   fs    │ │   http   │ │   path   │     │
│  │ファイル │ │ HTTP     │ │ パス     │     │
│  │ 操作    │ │ サーバー │ │ 処理     │     │
│  └─────────┘ └──────────┘ └──────────┘     │
└─────────────────────────────────────────────┘

┌─────────────────────────────────────────────┐
│          libuv イベントループライブラリ        │
│      クロスプラットフォームの非同期 I/O 対応   │
└─────────────────────────────────────────────┘

3.2 Node.js 固有の機能

1. ファイルシステム操作

javascript
const fs = require('fs')

// ファイルの読み込み
fs.readFile('./data.txt', 'utf8', (err, data) => {
  if (err) throw err
  console.log(data)
})

// ファイルの書き込み
fs.writeFile('./output.txt', 'Hello', (err) => {
  if (err) throw err
  console.log('書き込み成功')
})

2. HTTP サーバー

javascript
const http = require('http')

const server = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/html' })
  res.end('<h1>Hello World</h1>')
})

server.listen(3000)

3. モジュールシステム

javascript
// CommonJS (Node.js のデフォルト)
const fs = require('fs')
module.exports = { myFunction }

// ES Modules (モダンな方法)
import fs from 'fs'
export { myFunction }

3.3 ブラウザ vs Node.js の比較

特徴ブラウザNode.js
エントリファイルHTML ファイルJavaScript ファイル
グローバルオブジェクトwindow, documentglobal, process
モジュール読み込み<script> タグrequire() / import
セキュリティサンドボックス環境、制限ありシステムリソースにアクセス可能
用途ユーザーインターフェースバックエンドサービス、ツール

4. イベントループの深掘り

🤔 核心の問い

JavaScript はシングルスレッドなのに、なぜ「ブロックしない」動作ができるのか?

4.1 イベントループとは何か

イベントループ = JavaScript の「タスクスケジュールセンター」

JavaScript はシングルスレッドであり、一度に一つのことしかできません。しかし、イベントループのおかげで、多くのことを「同時に」行っているように見せることができます。

中心的な仕組み:

  1. 同期コードの実行(コールスタック)
  2. 非同期タスクの処理(タスクキュー)
  3. 新しいタスクの待機(繰り返し)
コールスタック              タスクキュー
┌─────────┐              ┌──────────┐
│ タスク 1 │              │ マクロ 1  │
│ タスク 2 │ ←──────────── │ マクロ 2  │
│ タスク 3 │   一つ実行完了 │ マクロ 3  │
└─────────┘   したら次取得  └──────────┘
      ↓                        ↑
      └────────────────────────┘
         イベントループが継続的にチェック

4.2 マクロタスク vs マイクロタスク

これは面接や実際の開発で最も混同しやすい概念です!

マクロタスク (Macrotask):

  • setTimeout, setInterval
  • I/O 操作
  • UI レンダリング

マイクロタスク (Microtask):

  • Promise.then
  • MutationObserver
  • queueMicrotask

実行順序:同期コード → マイクロタスク → マクロタスク

👇 試してみましょう:マクロタスクとマイクロタスクの実行順序を観察する

任务队列:宏任务 vs 微任务

代码示例

1console.log("1")同步
2setTimeout(() => console.log("2"), 0)宏任务
3Promise.resolve().then(() => console.log("3"))微任务
4console.log("4")同步
5setTimeout(() => console.log("5"), 0)宏任务

调用栈 (正在执行)

执行 console.log("1")

微任务队列 Microtask

队列为空

宏任务队列 Macrotask

队列为空

输出日志 (执行顺序)

等待输出...

执行顺序规则

1执行所有同步代码
2执行微任务队列中的所有任务
3执行一个宏任务
4重复步骤 2-3

核心要点: 微任务优先级高于宏任务。每次执行完一个宏任务后,都会检查并执行所有微任务,然后再执行下一个宏任务。

4.3 クラシックな面接問題

javascript
console.log('1')

setTimeout(() => console.log('2'), 0)

Promise.resolve().then(() => console.log('3'))

console.log('4')

// 出力: 1, 4, 3, 2

なぜこの順序なのか?

  1. 同期コードの実行:console.log('1')console.log('4') → 1, 4 を出力
  2. マイクロタスクキューの確認:Promise.then → 3 を出力
  3. マクロタスクキューの確認:setTimeout → 2 を出力

💡 実践的なヒント

  • コードをできるだけ早く実行したい場合、マイクロタスク(Promise.then)を使う
  • 実行を遅らせたい場合、マクロタスク(setTimeout)を使う
  • 多すぎる非同期操作を混ぜないこと。そうしないと「コールバック地獄」に陥る

5. コールスタックとメモリ

🤔 核心の問い

コードはどのように実行されるのか?変数はどこに格納されるのか?いつ回収されるのか?

5.1 コールスタック:関数実行の「足跡」

コールスタック = 関数呼び出しを記録する「ノート」

関数を呼び出すたびに、スタックに新しいレコードが追加されます。関数の実行が完了すると、そのレコードは削除されます。

javascript
function a() {
  b()
}

function b() {
  c()
}

function c() {
  console.log('実行完了')
}

a()

コールスタックの変化:

ステップ 1: a() を呼び出し
┌─────────┐
│    a    │
└─────────┘

ステップ 2: a() が b() を呼び出し
┌─────────┐
│    b    │
│    a    │
└─────────┘

ステップ 3: b() が c() を呼び出し
┌─────────┐
│    c    │
│    b    │
│    a    │
└─────────┘

ステップ 4: c() が完了、順番にポップ
┌─────────┐
│    b    │
│    a    │
└─────────┘

👇 試してみましょう:コールスタックの変化を観察する

调用栈:函数执行的足迹

代码

1main()
2function a() {
3function b() {
4function c() {
5console.log("执行完毕")
6}
7}
8}
9}

调用栈

栈底
栈为空
栈顶

当前状态:

调用 main()

输出

等待输出...

调用栈工作原理:

  • 每次调用函数,就会在栈上"压入"一个新的"栈帧"
  • 栈帧记录了函数的执行状态、局部变量等信息
  • 函数执行完毕,栈帧就会从栈上"弹出"
  • 栈是"后进先出"(LIFO)的数据结构
  • 如果递归太深,会导致"栈溢出"错误

调用栈就像一摞盘子:最后放上去的盘子最先被取走。每个函数就是一个盘子,执行完就取走,然后继续执行下面的函数。

5.2 メモリ管理:ガベージはどこへ行くのか

JavaScript には「自動ガベージコレクション」の仕組みがあります——手動でメモリを解放する必要はなく、エンジンが代わりに行ってくれます。

ガベージコレクションの原理:マークアンドスイープアルゴリズム

  1. マークフェーズ:「ルート」から開始し、到達可能なすべての変数を見つける
  2. スイープフェーズ:マークされていない変数は「ガベージ」であり、回収される
javascript
// ガベージコレクションの例
let obj1 = { name: 'オブジェクト1' }
let obj2 = { name: 'オブジェクト2' }

// obj1 が再代入され、元のオブジェクトは参照を失う
obj1 = null  // 元の { name: 'オブジェクト1' } は回収される

// obj2 はまだ使用中、回収されない
console.log(obj2.name)

👇 試してみましょう:ガベージコレクションのプロセスを観察する

垃圾回收机制

标记阶段从根对象开始,标记所有可达对象
清除阶段回收未标记的对象

对象引用关系

未标记
已标记(可达)
已回收
🌳
Root
📦
obj1
📦
obj2
📦
obj3
📦
obj4
📦
obj5
📦
obj6
当前操作:从根对象开始标记

标记-清除算法 (Mark-and-Sweep)

1
标记阶段

从根对象(Root)开始,遍历所有可达对象,标记为"活动对象"

2
清除阶段

遍历整个堆内存,回收所有未被标记的对象

3
重置标记

清除所有标记位,为下一次垃圾回收做准备

核心要点
  • 根对象(Root): 全局变量、栈上的变量等,总是被认为是可达的
  • 可达对象: 从根对象出发,通过引用链能访问到的对象
  • 垃圾对象: 无法从根对象访问到的对象,会被回收
  • 循环引用: 如果两个对象互相引用但都不可达,仍会被回收

实际应用技巧

💡
及时解除引用

对象不再使用时,将其设为 null

🔒
避免意外的全局变量

使用 const/let 代替 var

🧹
清理事件监听

组件销毁时移除所有监听器

📊
定期检查内存

用 DevTools Memory 面板监控

5.3 メモリリーク:クリーンアップを忘れた結果

メモリリーク = 解放されるべきメモリが解放されず、蓄積されていくこと

よくある原因:

1. グローバル変数が多すぎる

javascript
// ❌ 間違い:グローバル変数は回収されない
globalCache = []

function addItem(item) {
  globalCache.push(item)
}

2. イベントリスナーが削除されていない

javascript
// ❌ 間違い:リスナーが削除されていない
button.addEventListener('click', handleClick)

// ✅ 正しい:不要になったらリスナーを削除
button.removeEventListener('click', handleClick)

3. クロージャが大きなオブジェクトを参照している

javascript
// ❌ 間違い:クロージャが大きなオブジェクトを参照し続け、回収されない
function createHandler() {
  const bigData = new Array(1000000).fill('data')
  return function() {
    console.log('処理中')
  }
}

const handler = createHandler()  // bigData がメモリに残り続ける

👇 試してみましょう:メモリリークがどのように発生するかを観察する

内存泄漏演示

内存使用情况0%

全局变量泄漏

问题:全局变量不会被垃圾回收,会一直占用内存

示例:不断往全局数组添加数据,从不清理

全局变量 (0 项)
暂无全局变量
❌ 错误做法
// 全局变量不会被回收
globalCache = []
function addItem() {
  globalCache.push(largeData)
}

如何避免内存泄漏

  • 避免全局变量: 使用 const/let 代替 var,尽量使用局部变量
  • 及时清理监听器: 组件销毁时移除所有事件监听
  • 释放闭包引用: 不需要时将闭包变量设为 null
  • 使用 WeakMap/WeakSet: 自动清理不再被引用的对象
  • 定期检查: 用 DevTools Memory 面板检查内存泄漏

💡 実践的なヒント

  • 定期的なチェック:ブラウザの DevTools → Memory → Take Heap Snapshot を開き、メモリ使用量を確認
  • グローバル変数の回避var ではなく constlet を使う
  • こまめなクリーンアップ:イベントリスナーやタイマーは使い終わったら削除
  • 弱参照WeakMapWeakSet を使ってオブジェクトの参照を保存

6. 実践的なテクニック

🤔 核心の問い

高性能な JavaScript コードの書き方は?問題が発生したときのデバッグ方法は?

6.1 パフォーマンス最適化のヒント

1. リフローとリペイントの削減

javascript
// ❌ 間違い:ループのたびにリフローが発生
for (let i = 0; i < 1000; i++) {
  element.style.top = i + 'px'
}

// ✅ 正しい:一括変更
element.style.transform = `translateY(${position}px)`

2. イベント委譲の使用

javascript
// ❌ 間違い:各ボタンにリスナーを追加
buttons.forEach(btn => {
  btn.addEventListener('click', handleClick)
})

// ✅ 正しい:親要素に一つだけリスナーを追加
container.addEventListener('click', (e) => {
  if (e.target.matches('.button')) {
    handleClick(e)
  }
})

3. デバウンスとスロットル

javascript
// デバウンス:ユーザーが入力を止めた後に実行
function debounce(fn, delay) {
  let timer
  return function(...args) {
    clearTimeout(timer)
    timer = setTimeout(() => fn.apply(this, args), delay)
  }
}

// スロットル:実行頻度を制限
function throttle(fn, delay) {
  let lastTime = 0
  return function(...args) {
    const now = Date.now()
    if (now - lastTime >= delay) {
      fn.apply(this, args)
      lastTime = now
    }
  }
}

6.2 デバッグのヒント

1. DevTools でコールスタックを確認

javascript
function a() {
  b()
}

function b() {
  c()
}

function c() {
  debugger  // ここで一時停止、コールスタックを確認
}

a()

2. console.trace() で実行パスを追跡

javascript
function trackExecution() {
  console.trace('実行パス')
  // 完全なコールスタックが出力される
}

3. Performance API でパフォーマンスを分析

javascript
performance.mark('start')

// 何らかのコードを実行
for (let i = 0; i < 10000; i++) {
  // ...
}

performance.mark('end')
performance.measure('ループパフォーマンス', 'start', 'end')

const measure = performance.getEntriesByName('ループパフォーマンス')[0]
console.log(`実行時間: ${measure.duration}ms`)

6.3 よくある問題のクイックリファレンス

問題考えられる原因解決策
メモリ使用量が高いメモリリーク、キャッシュ過多グローバル変数の確認、リスナーの削除
ページがカクつく長時間タスクがメインスレッドをブロックタスクの分割、Web Workers の使用
イベントが発火しないリスナーがバインドされていない、要素が存在しないDOM の読み込みタイミングを確認
非同期の順序がおかしいマクロタスクとマイクロタスクの混用Promise または async/await に統一
タイマーが不正確メインスレッドのブロックWeb Workers または requestAnimationFrame を使用

まとめ

これで次のことが理解できるはずです:

  • ランタイム = エンジン + 環境 API — 異なるランタイムは異なる機能を提供する
  • イベントループは同期コード、マイクロタスク、マクロタスクの実行順序を調整する
  • コールスタックは関数の実行過程を記録し、スタックオーバーフローは再帰が深すぎることが原因
  • ガベージコレクションは未使用の変数を自動的にクリーンアップするが、メモリリークに注意
  • パフォーマンス最適化の鍵はリフロー/リペイントの削減と非同期の適切な使用

💡 問題に遭遇したときの AI への伝え方

  • 「この関数の実行が遅すぎる、パフォーマンスの最適化を手伝ってほしい」
  • 「メモリ使用量が増え続けている、メモリリークの可能性があるので確認してほしい」
  • 「非同期操作の順序が間違っている、A の次に B であるべきなのに、A と B がほぼ同時に始まっている」
  • 「イベントリスナーが発火しない、要素がすでに DOM に読み込まれているか確認してほしい」