JavaScript Runtime In-Depth Guide
Preface
You've already learned JavaScript basics, but have you ever wondered:
- Where exactly does your code run?
- Why does the same code behave differently in the browser versus Node.js?
- Why does code sometimes "freeze up," while at other times it seems to run "in parallel"?
This article will take you deep into the JavaScript runtime environment, including the event loop, call stack, memory management, and more. After reading this, you'll understand why code executes in a particular order, quickly locate async-related bugs, optimize code performance, and avoid memory leaks.
What will you learn in this article?
| Chapter | Content | What you'll be able to do |
|---|---|---|
| Chapter 1 | Runtime overview | Understand where JavaScript code runs |
| Chapter 2 | Browser runtime | Know what Web APIs the browser provides |
| Chapter 3 | Node.js runtime | Understand the server-side JavaScript environment |
| Chapter 4 | Event loop deep dive | Master the execution order of macrotasks and microtasks |
| Chapter 5 | Call stack and memory | Understand code execution and memory management |
| Chapter 6 | Practical tips | Optimize performance and debug memory leaks |
1. Runtime Overview
🤔 Core Question
What is a "runtime"? JavaScript is just a language — why does the same code behave differently in different environments?
1.1 What is a Runtime?
Runtime = JavaScript Engine + Environment-provided APIs
If JavaScript is the "programming language," then the runtime is the "operating system" — it determines what your code can and cannot do.
┌─────────────────────────────────────┐
│ JavaScript Code │
├─────────────────────────────────────┤
│ JavaScript Engine (V8) │ ← Responsible for parsing and executing code
├─────────────────────────────────────┤
│ Runtime Environment (Browser/Node.js) │ ← Provides additional capabilities
└─────────────────────────────────────┘An analogy: JavaScript is "Mandarin," the runtime is the "city"
- JavaScript syntax (Mandarin) is the same everywhere
- But different cities provide different facilities:
- Browser = has DOM, window, fetch (like a city with malls, libraries)
- Node.js = has fs, http, path (like a city with factories, highways)
1.2 Two Mainstream Runtimes
| Feature | Browser | Node.js |
|---|---|---|
| Primary use | Web interaction, user interfaces | Server-side applications, CLI tools |
| Global object | window | global |
| DOM API | ✅ Supported | ❌ Not supported |
| File system | ❌ Limited | ✅ Full support |
| Module system | ES Modules | CommonJS + ES Modules |
| Timers | setTimeout, setInterval | setTimeout, setInterval |
| Network requests | fetch, XMLHttpRequest | http, https modules |
👇 Try it out: Compare the environment differences between the browser and Node.js
运行时环境对比
浏览器环境
- ✅ 有 DOM 和 BOM API,可以操作网页
- ✅ 有 Web Storage (localStorage, sessionStorage)
- ✅ 有 fetch 和 XMLHttpRequest 进行网络请求
- ❌ 没有文件系统访问权限
- ❌ 不能直接创建 HTTP 服务器
代码演示:不同环境的差异
核心区别:
浏览器运行时专注于用户界面和网页交互,提供 DOM、BOM、fetch 等前端专用 API。
Node.js 运行时专注于服务器端开发,提供文件系统、HTTP 服务器、进程管理等后端专用 API。
同样的 JavaScript 语法,但能用的 API 完全不同——这就是"环境判断"的重要性。
💡 Core Takeaway
The runtime determines which APIs you can use. DOM APIs available in the browser won't work in Node.js; file APIs available in Node.js won't work in the browser. That's why some code needs "environment detection."
2. Browser Runtime
🤔 Core Question
What capabilities does the browser provide for JavaScript to manipulate web pages?
2.1 Components of the Browser Runtime
┌─────────────────────────────────────────────┐
│ JavaScript Engine │
│ (V8 / SpiderMonkey) │
└─────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────┐
│ Web APIs │
│ ┌─────────┐ ┌──────────┐ ┌──────────┐ │
│ │ DOM │ │ BOM │ │ Network │ │
│ │Manipulate│ │Manipulate│ │ Network │ │
│ │ pages │ │ browser │ │ requests │ │
│ └─────────┘ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────┐
│ Event Loop │
│ Coordinates code execution, event │
│ handling, and task scheduling │
└─────────────────────────────────────────────┘2.2 Three Categories of Web APIs
1. DOM API - Manipulate page content
// Find elements
const title = document.querySelector('h1')
// Modify content
title.textContent = 'New Title'
// Add styles
title.style.color = 'red'2. BOM API - Manipulate the browser
// Page navigation
window.location.href = 'https://example.com'
// Browser storage
localStorage.setItem('key', 'value')
// Browser history
history.back()3. Network API - Network requests
// Send HTTP request
fetch('/api/data')
.then(response => response.json())
.then(data => console.log(data))2.3 Browser-Specific Event Mechanism
One of the most powerful features of the browser runtime is "event-driven" programming — code doesn't need to run continuously, but executes when the user performs actions.
button.addEventListener('click', () => {
console.log('Button was clicked')
})Common event types:
| Event type | When triggered | Practical scenario |
|---|---|---|
click | Mouse click | Button interaction |
input | Input field content changes | Real-time search |
scroll | Page scrolling | Lazy loading |
load | Resource finished loading | Initialize data |
error | Error occurred | Error handling |
3. Node.js Runtime
🤔 Core Question
What enables JavaScript to run on the server side?
3.1 Components of Node.js
┌─────────────────────────────────────────────┐
│ JavaScript Engine │
│ (V8) │
└─────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────┐
│ Node.js Built-in Modules │
│ ┌─────────┐ ┌──────────┐ ┌──────────┐ │
│ │ fs │ │ http │ │ path │ │
│ │ File │ │ HTTP │ │ Path │ │
│ │operations│ │ server │ │ handling │ │
│ └─────────┘ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────┐
│ libuv Event Loop Library │
│ Cross-platform async I/O support │
└─────────────────────────────────────────────┘3.2 Node.js-Specific Capabilities
1. File System Operations
const fs = require('fs')
// Read file
fs.readFile('./data.txt', 'utf8', (err, data) => {
if (err) throw err
console.log(data)
})
// Write file
fs.writeFile('./output.txt', 'Hello', (err) => {
if (err) throw err
console.log('Write successful')
})2. HTTP Server
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. Module System
// CommonJS (Node.js default)
const fs = require('fs')
module.exports = { myFunction }
// ES Modules (modern approach)
import fs from 'fs'
export { myFunction }3.3 Browser vs Node.js Comparison
| Feature | Browser | Node.js |
|---|---|---|
| Entry file | HTML file | JavaScript file |
| Global objects | window, document | global, process |
| Module loading | <script> tags | require() / import |
| Security | Sandbox environment, restricted | Can access system resources |
| Use case | User interfaces | Backend services, tools |
4. Event Loop Deep Dive
🤔 Core Question
JavaScript is single-threaded — how does it achieve "non-blocking" behavior?
4.1 What is the Event Loop?
Event Loop = JavaScript's "task scheduling center"
JavaScript is single-threaded and can only do one thing at a time. But the event loop makes it appear to do many things "simultaneously."
Core mechanism:
- Execute synchronous code (call stack)
- Process asynchronous tasks (task queues)
- Wait for new tasks (loop continuously)
Call Stack Task Queue
┌─────────┐ ┌──────────┐
│ Task 1 │ │ Macro 1 │
│ Task 2 │ ←──────────── │ Macro 2 │
│ Task 3 │ After one │ Macro 3 │
└─────────┘ completes, └──────────┘
↓ take next ↑
└──────────────────────────┘
Event loop checks continuously4.2 Macrotasks vs Microtasks
This is the concept most easily confused in interviews and actual development!
Macrotasks:
setTimeout,setInterval- I/O operations
- UI rendering
Microtasks:
Promise.thenMutationObserverqueueMicrotask
Execution order: Synchronous code → Microtasks → Macrotasks
👇 Try it out: Observe the execution order of macrotasks and microtasks
任务队列:宏任务 vs 微任务
代码示例
调用栈 (正在执行)
微任务队列 Microtask
宏任务队列 Macrotask
输出日志 (执行顺序)
执行顺序规则
核心要点: 微任务优先级高于宏任务。每次执行完一个宏任务后,都会检查并执行所有微任务,然后再执行下一个宏任务。
4.3 Classic Interview Question
console.log('1')
setTimeout(() => console.log('2'), 0)
Promise.resolve().then(() => console.log('3'))
console.log('4')
// Output: 1, 4, 3, 2Why this order?
- Execute synchronous code:
console.log('1'),console.log('4')→ outputs 1, 4 - Check microtask queue:
Promise.then→ outputs 3 - Check macrotask queue:
setTimeout→ outputs 2
💡 Practical Tips
- If you want code to execute as soon as possible, use microtasks (
Promise.then) - If you want delayed execution, use macrotasks (
setTimeout) - Never mix too many async operations, or you'll fall into "callback hell"
5. Call Stack and Memory
🤔 Core Question
How is code executed? Where are variables stored? When are they garbage collected?
5.1 Call Stack: The "Footprint" of Function Execution
Call stack = A "notebook" that records function calls
Every time you call a function, a new record is added to the stack; when the function finishes, the record is removed.
function a() {
b()
}
function b() {
c()
}
function c() {
console.log('Execution complete')
}
a()Call stack changes:
Step 1: Call a()
┌─────────┐
│ a │
└─────────┘
Step 2: a() calls b()
┌─────────┐
│ b │
│ a │
└─────────┘
Step 3: b() calls c()
┌─────────┐
│ c │
│ b │
│ a │
└─────────┘
Step 4: c() completes, pop in order
┌─────────┐
│ b │
│ a │
└─────────┘👇 Try it out: Observe the call stack changes
调用栈:函数执行的足迹
代码
调用栈
当前状态:
调用 main()
输出
调用栈工作原理:
- 每次调用函数,就会在栈上"压入"一个新的"栈帧"
- 栈帧记录了函数的执行状态、局部变量等信息
- 函数执行完毕,栈帧就会从栈上"弹出"
- 栈是"后进先出"(LIFO)的数据结构
- 如果递归太深,会导致"栈溢出"错误
调用栈就像一摞盘子:最后放上去的盘子最先被取走。每个函数就是一个盘子,执行完就取走,然后继续执行下面的函数。
5.2 Memory Management: Where Does the Garbage Go?
JavaScript has an "automatic garbage collection" mechanism — you don't need to manually free memory; the engine does it for you.
Garbage collection principle: Mark-and-Sweep algorithm
- Mark phase: Starting from "roots," find all reachable variables
- Sweep phase: Unmarked variables are "garbage" and will be collected
// Garbage collection example
let obj1 = { name: 'Object 1' }
let obj2 = { name: 'Object 2' }
// obj1 is reassigned, the original object loses its reference
obj1 = null // The original { name: 'Object 1' } will be collected
// obj2 is still in use, won't be collected
console.log(obj2.name)👇 Try it out: Observe the garbage collection process
垃圾回收机制
对象引用关系
标记-清除算法 (Mark-and-Sweep)
从根对象(Root)开始,遍历所有可达对象,标记为"活动对象"
遍历整个堆内存,回收所有未被标记的对象
清除所有标记位,为下一次垃圾回收做准备
核心要点
- 根对象(Root): 全局变量、栈上的变量等,总是被认为是可达的
- 可达对象: 从根对象出发,通过引用链能访问到的对象
- 垃圾对象: 无法从根对象访问到的对象,会被回收
- 循环引用: 如果两个对象互相引用但都不可达,仍会被回收
实际应用技巧
对象不再使用时,将其设为 null
使用 const/let 代替 var
组件销毁时移除所有监听器
用 DevTools Memory 面板监控
5.3 Memory Leaks: The Consequences of Forgetting to Clean Up
Memory leak = Memory that should be freed isn't freed, accumulating over time
Common causes:
1. Too many global variables
// ❌ Wrong: Global variables won't be collected
globalCache = []
function addItem(item) {
globalCache.push(item)
}2. Event listeners not removed
// ❌ Wrong: Listener not removed
button.addEventListener('click', handleClick)
// ✅ Correct: Remove listener when no longer needed
button.removeEventListener('click', handleClick)3. Closures referencing large objects
// ❌ Wrong: Closure keeps referencing large object, won't be collected
function createHandler() {
const bigData = new Array(1000000).fill('data')
return function() {
console.log('Processing')
}
}
const handler = createHandler() // bigData persists in memory👇 Try it out: Observe how memory leaks occur
内存泄漏演示
全局变量泄漏
问题:全局变量不会被垃圾回收,会一直占用内存
示例:不断往全局数组添加数据,从不清理
❌ 错误做法
// 全局变量不会被回收
globalCache = []
function addItem() {
globalCache.push(largeData)
}如何避免内存泄漏
- 避免全局变量: 使用 const/let 代替 var,尽量使用局部变量
- 及时清理监听器: 组件销毁时移除所有事件监听
- 释放闭包引用: 不需要时将闭包变量设为 null
- 使用 WeakMap/WeakSet: 自动清理不再被引用的对象
- 定期检查: 用 DevTools Memory 面板检查内存泄漏
💡 Practical Tips
- Regular checks: Open browser DevTools → Memory → Take Heap Snapshot to view memory usage
- Avoid global variables: Use
constandlet, notvar - Clean up promptly: Remove event listeners and timers when done
- Weak references: Use
WeakMapandWeakSetto store object references
6. Practical Tips
🤔 Core Question
How do you write high-performance JavaScript code? How do you debug problems?
6.1 Performance Optimization Tips
1. Reduce reflows and repaints
// ❌ Wrong: Triggers reflow on every loop iteration
for (let i = 0; i < 1000; i++) {
element.style.top = i + 'px'
}
// ✅ Correct: Batch modification
element.style.transform = `translateY(${position}px)`2. Use event delegation
// ❌ Wrong: Add listener to every button
buttons.forEach(btn => {
btn.addEventListener('click', handleClick)
})
// ✅ Correct: Add only one listener to parent element
container.addEventListener('click', (e) => {
if (e.target.matches('.button')) {
handleClick(e)
}
})3. Debounce and throttle
// Debounce: Execute after user stops typing
function debounce(fn, delay) {
let timer
return function(...args) {
clearTimeout(timer)
timer = setTimeout(() => fn.apply(this, args), delay)
}
}
// Throttle: Limit execution frequency
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 Debugging Tips
1. View call stack with DevTools
function a() {
b()
}
function b() {
c()
}
function c() {
debugger // Pause here to view call stack
}
a()2. Trace execution path with console.trace()
function trackExecution() {
console.trace('Execution path')
// Will output the complete call stack
}3. Analyze performance with the Performance API
performance.mark('start')
// Execute some code
for (let i = 0; i < 10000; i++) {
// ...
}
performance.mark('end')
performance.measure('Loop performance', 'start', 'end')
const measure = performance.getEntriesByName('Loop performance')[0]
console.log(`Execution time: ${measure.duration}ms`)6.3 Common Problems Quick Reference
| Problem | Possible cause | Solution |
|---|---|---|
| High memory usage | Memory leak, too much caching | Check global variables, remove listeners |
| Page stuttering | Long tasks blocking main thread | Split tasks, use Web Workers |
| Events not firing | Listener not bound, element doesn't exist | Check DOM loading timing |
| Async order incorrect | Mixing macrotasks and microtasks | Use Promise or async/await consistently |
| Timer inaccuracy | Main thread blocked | Use Web Workers or requestAnimationFrame |
Summary
You should now be able to understand:
- Runtime = Engine + Environment APIs — different runtimes provide different capabilities
- Event loop coordinates the execution order of synchronous code, microtasks, and macrotasks
- Call stack records the function execution process — stack overflow occurs from too-deep recursion
- Garbage collection automatically cleans up unused variables, but watch out for memory leaks
- Performance optimization hinges on reducing reflows/repaints and using async appropriately
💡 When encountering problems, tell your AI this way
- "This function executes too slowly, help me optimize the performance"
- "Memory usage keeps growing, might be a memory leak, help me check"
- "Async operations are in the wrong order — should be A then B, but A and B start almost simultaneously"
- "Event listener isn't firing, check if the element has already been loaded into the DOM"