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 ツール |
| グローバルオブジェクト | window | global |
| DOM API | ✅ 対応 | ❌ 非対応 |
| ファイルシステム | ❌ 制限あり | ✅ 完全対応 |
| モジュールシステム | ES Modules | CommonJS + ES Modules |
| タイマー | setTimeout, setInterval | setTimeout, setInterval |
| ネットワークリクエスト | fetch, XMLHttpRequest | http, https モジュール |
👇 試してみましょう:ブラウザと Node.js の環境の違いを比較する
运行时环境对比
浏览器环境
- ✅ 有 DOM 和 BOM API,可以操作网页
- ✅ 有 Web Storage (localStorage, sessionStorage)
- ✅ 有 fetch 和 XMLHttpRequest 进行网络请求
- ❌ 没有文件系统访问权限
- ❌ 不能直接创建 HTTP 服务器
代码演示:不同环境的差异
核心区别:
浏览器运行时专注于用户界面和网页交互,提供 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 - ページの内容を操作
// 要素の検索
const title = document.querySelector('h1')
// 内容の変更
title.textContent = '新しいタイトル'
// スタイルの追加
title.style.color = 'red'2. BOM API - ブラウザを操作
// ページ遷移
window.location.href = 'https://example.com'
// ブラウザストレージ
localStorage.setItem('key', 'value')
// ブラウザ履歴
history.back()3. Network API - ネットワークリクエスト
// HTTP リクエストの送信
fetch('/api/data')
.then(response => response.json())
.then(data => console.log(data))2.3 ブラウザ特有のイベントメカニズム
ブラウザランタイムの最も強力な機能の一つは「イベント駆動」です——コードは常に実行され続けるのではなく、ユーザーの操作があったときに初めて実行されます。
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. ファイルシステム操作
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 サーバー
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. モジュールシステム
// 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, document | global, process |
| モジュール読み込み | <script> タグ | require() / import |
| セキュリティ | サンドボックス環境、制限あり | システムリソースにアクセス可能 |
| 用途 | ユーザーインターフェース | バックエンドサービス、ツール |
4. イベントループの深掘り
🤔 核心の問い
JavaScript はシングルスレッドなのに、なぜ「ブロックしない」動作ができるのか?
4.1 イベントループとは何か
イベントループ = JavaScript の「タスクスケジュールセンター」
JavaScript はシングルスレッドであり、一度に一つのことしかできません。しかし、イベントループのおかげで、多くのことを「同時に」行っているように見せることができます。
中心的な仕組み:
- 同期コードの実行(コールスタック)
- 非同期タスクの処理(タスクキュー)
- 新しいタスクの待機(繰り返し)
コールスタック タスクキュー
┌─────────┐ ┌──────────┐
│ タスク 1 │ │ マクロ 1 │
│ タスク 2 │ ←──────────── │ マクロ 2 │
│ タスク 3 │ 一つ実行完了 │ マクロ 3 │
└─────────┘ したら次取得 └──────────┘
↓ ↑
└────────────────────────┘
イベントループが継続的にチェック4.2 マクロタスク vs マイクロタスク
これは面接や実際の開発で最も混同しやすい概念です!
マクロタスク (Macrotask):
setTimeout,setInterval- I/O 操作
- UI レンダリング
マイクロタスク (Microtask):
Promise.thenMutationObserverqueueMicrotask
実行順序:同期コード → マイクロタスク → マクロタスク
👇 試してみましょう:マクロタスクとマイクロタスクの実行順序を観察する
任务队列:宏任务 vs 微任务
代码示例
调用栈 (正在执行)
微任务队列 Microtask
宏任务队列 Macrotask
输出日志 (执行顺序)
执行顺序规则
核心要点: 微任务优先级高于宏任务。每次执行完一个宏任务后,都会检查并执行所有微任务,然后再执行下一个宏任务。
4.3 クラシックな面接問題
console.log('1')
setTimeout(() => console.log('2'), 0)
Promise.resolve().then(() => console.log('3'))
console.log('4')
// 出力: 1, 4, 3, 2なぜこの順序なのか?
- 同期コードの実行:
console.log('1')、console.log('4')→ 1, 4 を出力 - マイクロタスクキューの確認:
Promise.then→ 3 を出力 - マクロタスクキューの確認:
setTimeout→ 2 を出力
💡 実践的なヒント
- コードをできるだけ早く実行したい場合、マイクロタスク(
Promise.then)を使う - 実行を遅らせたい場合、マクロタスク(
setTimeout)を使う - 多すぎる非同期操作を混ぜないこと。そうしないと「コールバック地獄」に陥る
5. コールスタックとメモリ
🤔 核心の問い
コードはどのように実行されるのか?変数はどこに格納されるのか?いつ回収されるのか?
5.1 コールスタック:関数実行の「足跡」
コールスタック = 関数呼び出しを記録する「ノート」
関数を呼び出すたびに、スタックに新しいレコードが追加されます。関数の実行が完了すると、そのレコードは削除されます。
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 │
└─────────┘👇 試してみましょう:コールスタックの変化を観察する
调用栈:函数执行的足迹
代码
调用栈
当前状态:
调用 main()
输出
调用栈工作原理:
- 每次调用函数,就会在栈上"压入"一个新的"栈帧"
- 栈帧记录了函数的执行状态、局部变量等信息
- 函数执行完毕,栈帧就会从栈上"弹出"
- 栈是"后进先出"(LIFO)的数据结构
- 如果递归太深,会导致"栈溢出"错误
调用栈就像一摞盘子:最后放上去的盘子最先被取走。每个函数就是一个盘子,执行完就取走,然后继续执行下面的函数。
5.2 メモリ管理:ガベージはどこへ行くのか
JavaScript には「自動ガベージコレクション」の仕組みがあります——手動でメモリを解放する必要はなく、エンジンが代わりに行ってくれます。
ガベージコレクションの原理:マークアンドスイープアルゴリズム
- マークフェーズ:「ルート」から開始し、到達可能なすべての変数を見つける
- スイープフェーズ:マークされていない変数は「ガベージ」であり、回収される
// ガベージコレクションの例
let obj1 = { name: 'オブジェクト1' }
let obj2 = { name: 'オブジェクト2' }
// obj1 が再代入され、元のオブジェクトは参照を失う
obj1 = null // 元の { name: 'オブジェクト1' } は回収される
// obj2 はまだ使用中、回収されない
console.log(obj2.name)👇 試してみましょう:ガベージコレクションのプロセスを観察する
垃圾回收机制
对象引用关系
标记-清除算法 (Mark-and-Sweep)
从根对象(Root)开始,遍历所有可达对象,标记为"活动对象"
遍历整个堆内存,回收所有未被标记的对象
清除所有标记位,为下一次垃圾回收做准备
核心要点
- 根对象(Root): 全局变量、栈上的变量等,总是被认为是可达的
- 可达对象: 从根对象出发,通过引用链能访问到的对象
- 垃圾对象: 无法从根对象访问到的对象,会被回收
- 循环引用: 如果两个对象互相引用但都不可达,仍会被回收
实际应用技巧
对象不再使用时,将其设为 null
使用 const/let 代替 var
组件销毁时移除所有监听器
用 DevTools Memory 面板监控
5.3 メモリリーク:クリーンアップを忘れた結果
メモリリーク = 解放されるべきメモリが解放されず、蓄積されていくこと
よくある原因:
1. グローバル変数が多すぎる
// ❌ 間違い:グローバル変数は回収されない
globalCache = []
function addItem(item) {
globalCache.push(item)
}2. イベントリスナーが削除されていない
// ❌ 間違い:リスナーが削除されていない
button.addEventListener('click', handleClick)
// ✅ 正しい:不要になったらリスナーを削除
button.removeEventListener('click', handleClick)3. クロージャが大きなオブジェクトを参照している
// ❌ 間違い:クロージャが大きなオブジェクトを参照し続け、回収されない
function createHandler() {
const bigData = new Array(1000000).fill('data')
return function() {
console.log('処理中')
}
}
const handler = createHandler() // bigData がメモリに残り続ける👇 試してみましょう:メモリリークがどのように発生するかを観察する
内存泄漏演示
全局变量泄漏
问题:全局变量不会被垃圾回收,会一直占用内存
示例:不断往全局数组添加数据,从不清理
❌ 错误做法
// 全局变量不会被回收
globalCache = []
function addItem() {
globalCache.push(largeData)
}如何避免内存泄漏
- 避免全局变量: 使用 const/let 代替 var,尽量使用局部变量
- 及时清理监听器: 组件销毁时移除所有事件监听
- 释放闭包引用: 不需要时将闭包变量设为 null
- 使用 WeakMap/WeakSet: 自动清理不再被引用的对象
- 定期检查: 用 DevTools Memory 面板检查内存泄漏
💡 実践的なヒント
- 定期的なチェック:ブラウザの DevTools → Memory → Take Heap Snapshot を開き、メモリ使用量を確認
- グローバル変数の回避:
varではなくconstとletを使う - こまめなクリーンアップ:イベントリスナーやタイマーは使い終わったら削除
- 弱参照:
WeakMapとWeakSetを使ってオブジェクトの参照を保存
6. 実践的なテクニック
🤔 核心の問い
高性能な JavaScript コードの書き方は?問題が発生したときのデバッグ方法は?
6.1 パフォーマンス最適化のヒント
1. リフローとリペイントの削減
// ❌ 間違い:ループのたびにリフローが発生
for (let i = 0; i < 1000; i++) {
element.style.top = i + 'px'
}
// ✅ 正しい:一括変更
element.style.transform = `translateY(${position}px)`2. イベント委譲の使用
// ❌ 間違い:各ボタンにリスナーを追加
buttons.forEach(btn => {
btn.addEventListener('click', handleClick)
})
// ✅ 正しい:親要素に一つだけリスナーを追加
container.addEventListener('click', (e) => {
if (e.target.matches('.button')) {
handleClick(e)
}
})3. デバウンスとスロットル
// デバウンス:ユーザーが入力を止めた後に実行
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 でコールスタックを確認
function a() {
b()
}
function b() {
c()
}
function c() {
debugger // ここで一時停止、コールスタックを確認
}
a()2. console.trace() で実行パスを追跡
function trackExecution() {
console.trace('実行パス')
// 完全なコールスタックが出力される
}3. Performance API でパフォーマンスを分析
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 に読み込まれているか確認してほしい」