Skip to content

MODULE 0.1 — JS ENGINE & RUNTIME: CORE CONCEPTS

Mục tiêu: Hiểu "máy tính chạy code như thế nào" ở mức đủ vẽ trên giấy, đủ trả lời phỏng vấn cơ bản, và đủ để tiếp thu các module đào sâu sau (0.1a, 0.1b).
Nguyên tắc: Nếu không vẽ được Event Loop và Rendering Pipeline trên giấy, chưa sang module sau.


1. Context

Vấn đề thực tế:
Bạn viết const obj = { a: 1 } — nhưng điều gì thực sự xảy ra trong RAM? Tại sao Promise.then chạy trước setTimeout(..., 0)? Tại sao app bị memory leak sau 2 giờ? Tại sao đoạn code này chạy nhanh, đoạn kia chậm?

Hậu quả nếu không hiểu:

  • Không thể debug memory leak vì không hiểu Garbage Collection.
  • Không thể viết code bất đồng bộ đúng vì không hiểu Event Loop.
  • Không thể lý giải tại sao React re-render hay tại sao app chậm sau deploy.

Output của module này:

  • Vẽ được Event Loop, V8 Pipeline, và Heap Layout trên giấy.
  • Giải thích được cơ chế V8 ở mức concept (không cần đọc flags).
  • Hiểu tại sao object shape consistency ảnh hưởng performance.

2. Prerequisites

  • Biết JavaScript cơ bản: variables, functions, objects, arrays.
  • Biết cách mở Chrome DevTools (Console, Elements).
  • Hiểu khái niệm stack và heap ở mức đại học (nếu chưa có, học trong 30 phút trước khi bắt đầu).

3. Knowledge Building

A. V8 Engine Pipeline (Tinh gộn — Concept Level)

JavaScript là ngôn ngữ dynamic — type của biến có thể thay đổi runtime. Nhưng CPU chỉ hiểu machine code. V8 phải biên dịch JS → machine code nhanh nhất có thể.

Pipeline (V8 v12+):

JavaScript Source


   Ignition (Bytecode Interpreter)

       ├──► Sparkplug (Baseline Compiler) ──► Machine Code (nhanh, không optimize)

       └──► Maglev (Mid-tier Optimizer) ────► Optimized Machine Code

                    └──► TurboFan (Top-tier Optimizer) ──► Highly Optimized Machine Code
  • Ignition: Biên dịch JS thành bytecode. Chạy ngay lập tức. Không optimize. Dùng khi code vừa được load hoặc khi function quá lớn để optimize.
  • Sparkplug: Biên dịch bytecode thành machine code nhanh (baseline). Không bỏ qua edge case. Là lớp "an toàn nhất".
  • Maglev: Optimizer tầm trung. Inlining nhỏ, constant folding, type specialization. Compile nhanh hơn TurboFan.
  • TurboFan: Optimizer cao cấp. Loop unrolling, escape analysis, speculative optimization dựa trên type feedback.

Tại sao cần 4 tier?
Trade-off giữa thời gian biên dịchtốc độ chạy. Nếu chỉ có TurboFan, app sẽ freeze 2 giây khi load để biên dịch. Nếu chỉ có Ignition, app chậm gấp 5–10 lần. V8 dùng tiering để "chạy ngay, optimize dần".

Self-Check: Mở chrome://version để xem V8 version. Không cần công cụ phức tạp ở module này.

B. Hidden Classes & Inline Caching (Tinh gộn)

Khi bạn tạo object:

js
const p1 = { x: 1, y: 2 };
const p2 = { x: 3, y: 4 };

V8 tạo một hidden class (shape/map) mô tả cấu trúc object. p1p2 chia sẻ cùng hidden class vì có cùng shape (x rồi y).

Nếu bạn làm:

js
const p3 = { x: 1, y: 2 };
p3.z = 3;  // Thêm property mới

V8 phải tạo hidden class mới. Nếu object shape không consistent, V8 không thể inline cache property access → mỗi lần truy cập property phải lookup dictionary → chậm 10–100x.

Quy tắc vàng: Giữ object shape consistent trong hot path. Không thêm/xóa property dynamic sau khi object đã được tạo.

Self-Check: Viết 2 đoạn code — một dùng shape consistent, một dùng shape inconsistent. Chạy trong console và so sánh (đừng benchmark nghiêm ngặt ở module này, chỉ cần cảm nhận sự khác biệt với performance.now() đơn giản).

C. Event Loop (Tinh gộn — Đúng thứ tự)

JavaScript là single-threaded nhưng có thể xử lý bất đồng bộ nhờ Event Loop.

Các thành phần:

  • Call Stack: Nơi thực thi synchronous code. LIFO.
  • Heap: Nơi lưu objects. Unstructured memory.
  • Web APIs: Các API browser cung cấp (setTimeout, fetch, DOM events). Chạy ngoài main thread.
  • Microtask Queue: Chứa callbacks từ Promise.then, MutationObserver, queueMicrotask. FIFO. Được xử lý ngay sau khi call stack rỗng, trước macrotask.
  • Macrotask Queue (Callback Queue): Chứa callbacks từ setTimeout, setInterval, setImmediate, I/O. FIFO. Được xử lý mỗi iteration của event loop, sau khi microtask queue rỗng.
  • Render Phase: Browser paint frame (60Hz/120Hz). Chạy sau khi cả call stack và microtask queue rỗng, nhưng có thể bị delay nếu task quá dài.

Thứ tự chính xác mỗi vòng lặp:

  1. Lấy 1 task từ Macrotask Queue (nếu có) → đẩy lên Call Stack → chạy.
  2. Khi Call Stack rỗng → xử lý tất cả Microtask Queue (cho đến khi rỗng).
  3. Kiểm tra Render Phase (nếu đến lúc paint frame).
  4. Lặp lại.

Self-Check: Dự đoán output đoạn code sau trước khi chạy:

js
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');

Kết quả: 1 4 3 2.

D. Memory Management (Tinh gộn)

  • Mark-and-Sweep: GC đánh dấu objects reachable từ root (global object, stack variables). Objects không reachable bị xóa.
  • Generational GC: Heap chia làm Young Generation (ngắn hạn) và Old Generation (dài hạn). Objects mới sinh ra ở Young. Nếu sống qua vài lần Minor GC, promoted sang Old.
  • WeakMap / WeakSet / WeakRef / FinalizationRegistry: Cho phép reference đến object mà không ngăn GC thu hồi. WeakMap key phải là object; nếu key không còn reference nào khác, entry tự động bị xóa.

Self-Check: Mở Chrome DevTools → Memory tab → chụp 1 snapshot đơn giản, tìm object của bạn trong Constructor view. Chỉ cần biết cách mở tab, chưa cần phân tích sâu.


4. Mental Model

Model chính: Event Loop là vòng lặp điều phối của một nhà hàng

  • Call Stack là bếp chính — chỉ nấu được 1 món tại một thời điểm (single-threaded).
  • Web APIs là các bếp phụ — nhận order rồi báo lại khi xong (setTimeout, fetch).
  • Microtask Queue là khay ưu tiên — món này phải phục vụ ngay sau khi bếp chính rảnh, trước khi lấy order mới.
  • Macrotask Queue là khay thường — phục vụ theo thứ tự, mỗi lần 1 món.
  • Render Phase là lúc bồi bàn dọn bàn — chỉ làm khi bếp chính và khay ưu tiên đều rỗng.

Giới hạn của model:

  • Không bao gồm Worker Threads (Web Workers chạy trên thread riêng).
  • Không mô tả chi tiết V8 pipeline (Ignition → TurboFan).
  • Chỉ mô tả 1 event loop; Node.js có nhiều phase khác (timers, I/O callbacks, idle, prepare, check, close callbacks).

Model phụ: Heap là kho chứa đồ đạc có 2 khu vực

  • Young Generation = Bãi đỗ xe tạm — xe vào ra liên tục, dọn dẹp nhanh (Minor GC/Scavenge).
  • Old Generation = Nhà kho lâu dài — đồ ít thay đổi, dọn dẹp chậm nhưng kỹ (Major GC/Mark-Sweep-Compact).

5. Visualization

A. Event Loop Timeline

Time →
│  sync()        │  Promise.then()  │  setTimeout()  │  Render  │
│  ████████████  │  ████            │                │          │
│  Call Stack    │  Microtask       │                │          │
│                │  Queue           │                │          │
│                │                  │  ████          │  ░░░░    │
│                │                  │  Macrotask     │  Paint   │

B. Memory Layout

┌─────────────────────────────────────────┐
│              STACK                      │
│  (primitive values, references,         │
│   execution contexts)                   │
│  LIFO, fast allocation/deallocation     │
├─────────────────────────────────────────┤
│              HEAP                       │
│  ┌──────────────┐  ┌─────────────────┐  │
│  │   YOUNG      │  │      OLD        │  │
│  │  Generation  │  │   Generation    │  │
│  │  (New Space) │  │   (Old Space)   │  │
│  │  Minor GC    │  │   Major GC      │  │
│  │  (Scavenge)  │  │   (Mark-Sweep)  │  │
│  └──────────────┘  └─────────────────┘  │
└─────────────────────────────────────────┘

C. V8 Tiering Decision Flow

Function called


┌─────────────┐
│  Ignition   │──► Bytecode (chạy ngay)
└──────┬──────┘
       │ gọi nhiều lần

┌─────────────┐
│  Sparkplug  │──► Baseline Machine Code
└──────┬──────┘
       │ gọi rất nhiều, type ổn định

┌─────────────┐
│   Maglev    │──► Optimized Code (nhanh, compile nhanh)
└──────┬──────┘
       │ hot path, complex loops

┌─────────────┐
│  TurboFan   │──► Highly Optimized Code (nhanh nhất, compile lâu)
└─────────────┘

6. Guided Example

Bài toán: Dự đoán output và giải thích thứ tự.

js
console.log('1');

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

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

console.log('4');

Think Aloud:

  1. console.log('1') — synchronous, chạy ngay. Output: 1.
  2. setTimeout(..., 0) — đăng ký callback với Web API. Sau 0ms, callback () => console.log('2') được đẩy vào Macrotask Queue. Không chạy ngay.
  3. Promise.resolve().then(...)then callback được đẩy vào Microtask Queue. Không chạy ngay vì call stack chưa rỗng.
  4. console.log('4') — synchronous. Output: 4.
  5. Call stack rỗng. Event loop kiểm tra Microtask Queue trước. Chạy () => console.log('3'). Output: 3.
  6. Microtask queue rỗng. Event loop kiểm tra Macrotask Queue. Chạy () => console.log('2'). Output: 2.

Kết quả: 1 4 3 2.

Tại sao không phải 1 2 3 4: setTimeout là macrotask; Promise.then là microtask. Microtask luôn chạy trước macrotask sau khi call stack rỗng.


7. Guided Questions

  • Nếu thêm Promise.resolve().then(() => console.log('5')) bên trong setTimeout callback, thứ tự thay đổi thế nào?
  • Tại sao setTimeout(..., 0) không chạy ngay lập tức?
  • Điều gì xảy ra nếu microtask queue có vòng lặp vô hạn? Render phase có chạy không?
  • Tại sao object shape consistency quan trọng đối với V8? Nếu API trả về object có property khác nhau mỗi lần, điều gì xảy ra?

8. Guided Practice

Bài tập: Viết một đoạn code tạo ra chuỗi output A B C D E theo thứ tự, sử dụng kết hợp console.log, setTimeout, và Promise.then.

Checklist:

  • [ ] Output đúng thứ tự A B C D E.
  • [ ] Giải thích được tại sao mỗi log chạy ở phase nào (sync / microtask / macrotask).
  • [ ] Vẽ timeline tương ứng trên giấy hoặc text.

Gợi ý cấu trúc:

js
console.log('A');
// ...

9. Independent Practice

Bài tập 1: Cho đoạn code sau, dự đoán output và giải thích.

js
async function foo() {
  console.log('A');
  await Promise.resolve();
  console.log('B');
}

console.log('C');
foo();
console.log('D');

Yêu cầu:

  • Không chạy code trước khi viết đáp án.
  • Giải thích await tương đương với gì trong Event Loop.
  • So sánh với Promise.then.

Bài tập 2 (Shape Consistency): Viết 2 function:

  • fastLoop: Tạo 1 triệu object cùng shape {x, y}.
  • slowLoop: Tạo 1 triệu object với shape khác nhau (thêm property z sau khi tạo).
  • Dùng performance.now() đo thời gian. Ghi nhận kết quả.

10. Edge Cases

A. eval()with phá vỡ hidden class optimization

js
function createPoint(x, y) {
  return { x, y };
}

// Fast: consistent shape
const p1 = createPoint(1, 2);
const p2 = createPoint(3, 4);

// Slow: with() forces dictionary mode
function createPointSlow(x, y) {
  const p = { x, y };
  with (p) { z = 3; }  // V8 bỏ hidden class, chuyển sang dictionary
  return p;
}

Thực tế production: Legacy code dùng with hoặc dynamic property access (obj[dynamicKey]) trong hot path làm app chậm đi rõ rệt. Refactor sang static property access.

B. Memory leak qua closure

js
function setup() {
  const hugeArray = new Array(1000000).fill('x');

  document.getElementById('btn').addEventListener('click', () => {
    console.log('clicked');  // Closure giữ reference đến hugeArray
  });
}

hugeArray không bị GC dù không dùng đến vì closure trong event listener giữ reference. Thực tế: Trong React, effect không cleanup event listener → leak tích lũy qua route navigation.

C. setTimeout không đảm bảo chính xác

js
const start = Date.now();
setTimeout(() => {
  console.log(Date.now() - start);  // Có thể > 0ms, thậm chí > 4ms
}, 0);

Browser throttle setTimeout trong background tab hoặc khi main thread bận. Thực tế: Không dùng setTimeout(..., 0) để đo thời gian chính xác; dùng performance.now().


11. Reflection

Teach Back Prompt: Giải thích cho một junior developer tưởng tượng: "Tại sao Promise.then chạy trước setTimeout(..., 0)?" Không được dùng từ "microtask" hoặc "macrotask" trong 30 giây đầu — phải dùng analogy nhà hàng.

Self-Check:

  • [ ] Tôi vẽ được Event Loop từ đầu đến cuối.
  • [ ] Tôi giải thích được V8 pipeline (Ignition → Sparkplug → Maglev → TurboFan) ở mức concept.
  • [ ] Tôi biết tại sao object shape consistency quan trọng.
  • [ ] Tôi mở được Chrome DevTools Memory tab và chụp snapshot.

12. Exit Exam

Bài thi:

  1. Dự đoán output 3 đoạn code bất đồng bộ khác nhau (kết hợp async/await, Promise, setTimeout). Giải thích bằng Event Loop.
  2. Viết một đoạn code gây memory leak bằng closure, sau đó fix nó.
  3. Benchmark hai đoạn code: một dùng object shape consistent, một dùng shape inconsistent. Giải thích kết quả.

Pass criteria:

  • Giải thích được vì sao Promise.then chạy trước setTimeout(..., 0).
  • Vẽ được Event Loop trên giấy.
  • Giải thích được trade-off 4 tier của V8.


MODULE 0.1b — EVENT LOOP & MEMORY PRODUCTION DEBUGGING

Mục tiêu: Debug production thực: memory leak qua DevTools, INP cao qua LoAF, event loop blocking qua Performance tab.
Prerequisite: Module 0.1 (gốc) — đã vẽ được Event Loop, đã biết Heap layout.
Teaching Model: 70/30. Scaffolding cao nhưng yêu cầu học viên tự dùng công cụ.


1. Context

Vấn đề thực tế:

  • App chậm dần sau 30 phút sử dụng — user phải refresh. Memory tab cho thấy heap tăng 200MB mỗi giờ.
  • INP (Interaction to Next Paint) của trang là 350ms — Google đánh giá "poor". Cần giảm xuống < 200ms.
  • Modal đóng rồi nhưng focus không chuyển, dropdown không mở — event handler bị delay vì microtask queue flood.

Hậu quả nếu không hiểu:

  • Dùng console.log để "đoán" leak thay vì dùng 3-snapshot technique.
  • Không biết cách dùng LoAF API để đo INP trên production.
  • Không hiểu tại sao MessageChannel yield tốt hơn setTimeout(..., 0) trong scheduler.

Output:

  • Tìm và fix memory leak bằng Chrome DevTools Memory tab.
  • Giảm INP bằng cách tối ưu event loop scheduling.
  • Hiểu chính xác HTML Event Loop processing model.

2. Prerequisites

  • Module 0.1 (gốc) — Event Loop, Heap, Call Stack.
  • Biết mở Chrome DevTools Performance và Memory tab.
  • Biết dùng performance.now().

3. Knowledge Building

A. HTML Event Loop Processing Model (Chính xác theo Spec)

Mỗi vòng lặp Event Loop (theo WHATWG HTML Standard) thực hiện các bước:

  1. Let oldestTask be the oldest task on one of the event loop's task queues. Run it. (Macrotask)
  2. Perform a microtask checkpoint: Xử lý tất cả microtasks cho đến khi queue rỗng (microtask có thể sinh thêm microtask).
  3. Update the rendering: Nếu đến lúc render frame (thường 60Hz/120Hz), thực hiện:
    • Update animations
    • Resize
    • Scroll
    • Media query
    • Animation frame callbacks (requestAnimationFrame)
    • Intersection Observer
    • Paint

Quan trọng: Render phase không chạy sau mỗi task. Nó chạy khi đến refresh rate của display và khi main thread rảnh.

Self-Check: Mở Chrome DevTools Performance tab → record → xem phần "Task" và "Animation Frame Fired". Quan sát thứ tự.

B. MessageChannel vs setTimeout vs queueMicrotask

APIQueueUse CaseNhược điểm
queueMicrotaskMicrotaskFlush state sau current taskCó thể flood, block render
setTimeout(..., 0)MacrotaskYield main threadThrottle 4ms, không chính xác
MessageChannelMacrotaskYield chính xác hơn setTimeoutKhó debug, không phổ biến
requestAnimationFrameRender stepAnimation, layout readKhông chạy nếu tab hidden

Tại sao React Scheduler dùng MessageChannel thay vì setTimeout?
setTimeout bị browser throttle tối thiểu 4ms (hoặc 1ms trong nested timer). MessageChannel postMessage không bị throttle, yield chính xác hơn.

Self-Check: Viết task queue dùng MessageChannel và đo độ trễ bằng performance.now().

C. 3-Snapshot Technique (Tìm Memory Leak)

Phương pháp chuẩn của Chrome DevTools team:

  1. Snapshot 1: Chụp khi app vừa load (baseline).
  2. Thực hiện hành động nghi ngờ gây leak (ví dụ: mở/đóng modal 10 lần, navigate qua lại 5 route).
  3. Snapshot 2: Chụp ngay sau hành động.
  4. Thực hiện hành động tương tự thêm lần nữa.
  5. Snapshot 3: Chụp sau lần thứ hai.
  6. So sánh Snapshot 3 với Snapshot 2: Objects nào tăng đều đặn giữa 2 và 3 là nghi phạm.

Cách đọc Memory tab:

  • Constructor view: Liệt kê objects theo constructor. Tìm (array), (closure), Detached HTMLDivElement.
  • Dominator tree: Object nào giữ reference đến phần lớn heap. Root của leak thường nằm đây.
  • Containment: Trace reference từ root → object. Tìm "retaining path".

Self-Check: Mở một trang web, chụp 3 snapshot theo quy trình trên. Tìm object tăng bất thường.

D. Long Animation Frames API (LoAF) & INP

INP (Interaction to Next Paint) đo thời gian từ user interaction đến frame paint tiếp theo. INP cao thường do:

  • Event handler chạy quá lâu (blocking main thread).
  • Nhiều microtask chạy sau event handler.
  • Layout thrashing (forced synchronous layout).

LoAF API (Chrome 123+, Origin Trial / Stable):

js
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log('Blocking duration:', entry.blockingDuration);
    console.log('Scripts:', entry.scripts);
  }
});
observer.observe({ type: 'long-animation-frame', buffered: true });

blockingDuration cho biết frame bị block bao lâu. scripts array cho biết script nào gây delay.

Self-Check: Chạy đoạn code trên trên production site, tìm frame có blockingDuration > 50ms.

E. Orinoco & Concurrent GC (V8)

V8 hiện đại (Orinoco project) thực hiện Major GC (Mark-Sweep-Compact) concurrentincremental:

  • Concurrent: GC mark trên helper thread, không block main thread hoàn toàn.
  • Incremental: Chia Major GC thành nhiều chunk nhỏ, xen kẽ với JS execution.
  • Compact: Sau sweep, heap bị phân mảnh. Compact di chuyển objects để tạo vùng nhớ liên tục.

Tuy nhiên, Minor GC (Scavenge) vẫn mostly stop-the-world vì nó nhanh (< 1ms).

Self-Check: Mở chrome://tracing hoặc DevTools Performance → xem "Major GC" và "Minor GC" events.


4. Mental Model

Model: Event Loop là dây chuyền lắp ráp xe hơi

  • Mỗi macrotask là một chiếc xe đi qua dây chuyền. Dây chuyền chỉ lắp được 1 xe tại một thời điểm.
  • Microtask là kiểm tra chất lượng — phải làm xong hết tất cả kiểm tra của xe hiện tại trước khi đưa xe tiếp theo vào.
  • Render là lúc chụp ảnh xe hoàn thành — chỉ chụp khi dây chuyền rảnh và đến giờ chụp (60 lần/giây).
  • Nếu kiểm tra chất lượng (microtask) quá lâu → bỏ lỡ giờ chụp ảnh (drop frame).

Giới hạn: Model này không thể hiện việc microtask có thể sinh thêm microtask (vòng lặp vô hạn).

Model: Memory Leak là vết dầu loang

  • Bạn nhìn thấy vết dầu (heap tăng) nhưng không biết từ đâu.
  • 3-Snapshot là chụp ảnh vết dầu ở 3 thời điểm — vết nào loang đều đặn là nguồn.
  • Dominator tree là bản đồ địa hình — tìm điểm cao nhất (root) mà từ đó dầu chảy xuống.

5. Visualization

A. HTML Event Loop Spec-compliant Flow

┌─────────────────────────────────────────┐
│         EVENT LOOP ITERATION            │
│                                         │
│  1. Pop 1 Macrotask                     │
│     └── Run (Call Stack)                │
│                                         │
│  2. Microtask Checkpoint                │
│     └── Run ALL microtasks until empty  │
│         (microtask can enqueue more)    │
│                                         │
│  3. Update Rendering (if needed)        │
│     ├── Animation Frame Callbacks (rAF) │
│     ├── Resize                          │
│     ├── Scroll                          │
│     ├── Intersection Observer           │
│     └── Paint / Composite               │
│                                         │
│  4. Idle Period (if any)                │
│     └── requestIdleCallback             │
└─────────────────────────────────────────┘

B. 3-Snapshot Leak Detection

T0: Snapshot 1 (Baseline)

     ▼  Action x 10
T1: Snapshot 2 (+1000 DetachedDiv)

     ▼  Action x 10
T2: Snapshot 3 (+1000 DetachedDiv)


Compare #3 vs #2:
  Constructor: Detached HTMLDivElement ▲ 1000
  Retaining Path: root → appState → modalStack → [div, div, div...]
  Root Cause: modalStack không pop khi unmount

C. INP Breakdown

User Click


Event Handler (150ms) ──► Task Long


Promise.then() x 50 (30ms) ──► Microtask Flood


Forced Layout (50ms) ──► Layout Thrashing


Next Paint ──► INP = 230ms (Poor)

6. Guided Example

Bài toán: Debug memory leak trong React component có modal.

jsx
// BEFORE: Leak
function App() {
  const [modals, setModals] = useState([]);

  const openModal = () => {
    const id = Date.now();
    const modal = document.createElement('div');
    modal.id = `modal-${id}`;
    document.body.appendChild(modal);
    setModals(prev => [...prev, id]);
  };

  const closeModal = (id) => {
    setModals(prev => prev.filter(m => m !== id));
    // BUG: Không remove DOM element khỏi body!
  };

  // ...
}

Think Aloud — RIMHVRP:

  1. Reproduce: Mở/đóng modal 10 lần. Heap tăng ~2MB.
  2. Isolate: Comment document.body.appendChild → không leak. Xác nhận DOM là nguồn.
  3. Measure: Chụp 3 snapshot. Snapshot 3 vs 2: Detached HTMLDivElement tăng 10.
  4. Hypothesis: closeModal xóa state nhưng không removeChild.
  5. Verify: Thêm document.body.removeChild(document.getElementById(\modal-${id}`))`. Chụp lại snapshot → detached div = 0.
  6. Fix:
jsx
const closeModal = (id) => {
  const el = document.getElementById(\`modal-${id}\`);
  if (el) document.body.removeChild(el);
  setModals(prev => prev.filter(m => m !== id));
};
  1. Postmortem: Thiếu cleanup DOM khi unmount. Action item: luôn audit DOM manipulation trong useEffect cleanup.

7. Guided Questions

  • Tại sao MessageChannel yield chính xác hơn setTimeout(..., 0)? Dùng Performance tab chứng minh.
  • Nếu microtask queue có 1000 item, điều gì xảy ra với INP? Tại sao?
  • Trong 3-snapshot technique, tại sao so sánh Snapshot 3 vs 2 thay vì 2 vs 1?
  • requestIdleCallback có thể bị delay vĩnh viễn — khi nào? Fallback là gì?

8. Guided Practice

Bài tập: Tìm memory leak thực tế.

Cho đoạn code sau:

js
class EventBus {
  constructor() {
    this.listeners = new Map();
  }
  on(event, cb) {
    if (!this.listeners.has(event)) this.listeners.set(event, []);
    this.listeners.get(event).push(cb);
  }
  off(event, cb) {
    const arr = this.listeners.get(event);
    if (arr) {
      const idx = arr.indexOf(cb);
      if (idx > -1) arr.splice(idx, 1);
    }
  }
}

const bus = new EventBus();

function setup() {
  const data = new Array(100000).fill('x');
  const handler = () => console.log(data.length);
  bus.on('update', handler);
  // Không gọi bus.off khi component unmount
}

Yêu cầu:

  • [ ] Chụp 3 snapshot (trước, sau 10 lần setup, sau 20 lần setup).
  • [ ] Xác định constructor nào tăng bất thường.
  • [ ] Trace retaining path đến root.
  • [ ] Fix leak.

9. Independent Practice

Bài tập 1 (LoAF & INP):
Viết một trang có button. Click button chạy heavy computation 200ms (vòng lặp tính toán). Dùng LoAF API đo blockingDuration. Sau đó refactor để chia nhỏ computation bằng scheduler.yield() hoặc setTimeout chunking. Đo lại INP.

Bài tập 2 (DevTools Memory):
Tìm một trang web bất kỳ (hoặc dùng code leak ở Guided Practice). Chụp heap snapshot. Tìm 3 loại object chiếm nhiều memory nhất. Giải thích tại sao chúng tồn tại.


10. Edge Cases

A. Detached DOM tree không bị GC vì referenced từ JS

js
const cache = [];
function createWidget() {
  const div = document.createElement('div');
  document.body.appendChild(div);
  cache.push(div); // Giữ reference
  document.body.removeChild(div); // DOM detached nhưng JS vẫn giữ
}

Thực tế: Caching DOM elements để "reuse" nhưng quên giới hạn cache size. Dùng WeakRef hoặc giới hạn LRU.

B. ResizeObserver không cleanup → leak

js
// React component
useEffect(() => {
  const ro = new ResizeObserver(cb);
  ro.observe(ref.current);
  // Missing: return () => ro.disconnect();
}, []);

Thực tế: Mỗi lần mount/unmount component mới tạo thêm observer. Sau 50 route transitions → 50 observers chạy song song.

C. queueMicrotask flood block render

js
function flood() {
  queueMicrotask(() => {
    console.log('microtask');
    flood(); // Vòng lặp vô hạn microtask
  });
}
flood();

Thực tế: State management library flush quá nhiều microtask → UI đóng băng. Fix: batch updates, dùng macrotask yield.


11. Reflection

Teach Back:
Hướng dẫn junior cách dùng 3-snapshot technique. Dùng ví dụ cụ thể: "App của tôi chậm dần sau khi mở nhiều modal. Tôi làm thế nào để biết có phải memory leak không?"

Self-Check:

  • [ ] Tôi chụp được 3 snapshot và so sánh constructor view.
  • [ ] Tôi đọc được dominator tree để tìm root cause.
  • [ ] Tôi dùng LoAF API để đo blocking duration.
  • [ ] Tôi giải thích được sự khác biệt MessageChannel vs setTimeout.

12. Exit Exam

Bài thi:

  1. Debug Memory Leak: Cho một repo có leak (detached DOM hoặc closure). Dùng DevTools Memory tab, áp dụng 3-snapshot technique, viết báo cáo gồm: constructor nghi ngờ, retaining path, fix.
  2. Optimize INP: Cho một trang có INP 400ms. Dùng Performance tab và LoAF API, xác định nguyên nhân (long task / microtask flood / layout thrashing). Viết ADR cho fix.
  3. Implement Scheduler: Viết một task queue đơn giản dùng MessageChannel để yield main thread. So sánh độ trễ với setTimeout(..., 0) bằng performance.now().

Pass criteria:

  • Tìm được leak trong < 30 phút không cần Google (chỉ dùng DevTools).
  • Giảm INP ít nhất 50% trong bài tập mẫu.
  • Giải thích được HTML Event Loop processing model theo spec.


MODULE 0.1a — V8 EXECUTION & OPTIMIZATION DEEP DIVE

Mục tiêu: Hiểu tận gốc V8 biên dịch và tối ưu code như thế nào; tự kiểm chứng bằng V8 flags; debug deoptimization trong production.
Prerequisite: Module 0.1 (gốc) — đã biết 4 tier, đã biết hidden class concept.
Teaching Model: 70/30. Scaffolding cao vì nội dung engine internals khá trừu tượng, nhưng yêu cầu học viên tự chạy benchmark và đọc log.


1. Context

Vấn đề thực tế:

  • App chạy nhanh ở local nhưng chậm trên production sau 1 giờ. CPU profile cho thấy một function đơn giản chiếm 40% thời gian — nó đã bị deoptimize.
  • Team debate: "Có nên dùng class field arrow function để bind this không?" — câu trả lời đúng phụ thuộc vào cách V8 optimize nó.
  • API trả về object shape khác nhau mỗi lần → hot path chạy 10x chậm hơn.

Hậu quả nếu không hiểu:

  • Không thể giải thích tại sao delete obj.key làm chậm toàn bộ object.
  • Không thể đọc --trace-deopt output để tìm function bị deoptimize.
  • Không thể viết code "V8-friendly" trong hot path.

Output:

  • Dùng node --trace-opt --trace-deopt để xác định function optimize/deoptimize.
  • Giải thích được monomorphic → polymorphic → megamorphic inline cache.
  • Viết code tận dụng hidden class transitions và tránh dictionary mode.

2. Prerequisites

  • Module 0.1 (gốc) — V8 Pipeline, Hidden Classes.
  • Biết chạy Node.js từ terminal.
  • Biết dùng performance.now() để benchmark đơn giản.

3. Knowledge Building

A. Speculative Optimization & Type Feedback

V8 không biết type của biến khi parse. Nhưng khi function chạy nhiều lần, V8 thu thập type feedback qua Inline Caches (IC).

Ví dụ:

js
function add(x, y) {
  return x + y;
}
  • Lần gọi 1–2: x, y là number → IC ghi nhận number + number.
  • Lần gọi 3–5: V8 đoán (speculate) rằng x, y luôn là number → TurboFan tạo optimized code cho number + number.
  • Lần gọi 6: add("a", "b") — type khác! → Deoptimize (bailout) về bytecode hoặc baseline.

Deoptimization reasons phổ biến:

  • wrong map (hidden class khác)
  • no caching (megamorphic)
  • value mismatch (type không khớp speculation)

Self-Check: Chạy node --trace-opt --trace-deopt script.js và đọc output.

B. Hidden Class Transitions & Dictionary Mode

Khi tạo object:

js
const p = { x: 1 };

V8 tạo Hidden Class C0. Khi thêm y:

js
p.y = 2;

V8 tạo transition C0 → C1. Nếu tạo object mới { x: 1, y: 2 }, nó dùng C1 ngay.

Transition Array:
Mỗi hidden class lưu mảng transitions (property → new hidden class). Đây là cách V8 "cache" shape mới.

Dictionary Mode:
Khi bạn delete p.x, V8 không thể giữ transition tree vì property bị xóa tạo "hole". Object chuyển sang dictionary mode — property lookup thành hash table → chậm 10x.

Tương tự, obj[dynamicKey] = value (computed property) cũng có thể buộc dictionary mode nếu key không predictable.

Self-Check: Chạy benchmark so sánh object có delete và không có delete.

C. Inline Cache (IC) Hierarchy

Khi truy cập obj.x:

  1. Monomorphic (1 shape): IC cache 1 hidden class. Nếu khớp → lấy offset ngay lập tức. Nhanh nhất.
  2. Polymorphic (2–4 shapes): IC cache 2–4 hidden classes. Dùng switch nhỏ. Vẫn nhanh.
  3. Megamorphic (>4 shapes): IC bỏ cache. Mỗi lần access phải traverse prototype hoặc lookup dictionary. Chậm nhất.

Ví dụ thực tế:

js
function getX(obj) {
  return obj.x;
}

getX({ x: 1 });     // Monomorphic
getX({ x: 2, y: 3 }); // Different shape? → Polymorphic
getX([1, 2, 3]);    // Array → Polymorphic
getX("string");     // String → Megamorphic (hoặc polymorphic nếu chỉ 4)

Self-Check: Dùng %HaveSameMap(a, b) (với flag --allow-natives-syntax) để kiểm tra 2 object có cùng hidden class không.

D. V8 Flags & Benchmarking

Các flag hữu ích:

  • --trace-opt: Log khi nào function được optimize.
  • --trace-deopt: Log khi nào function bị deoptimize.
  • --allow-natives-syntax: Cho phép dùng %GetOptimizationStatus(fn).
  • --print-opt-code: In optimized assembly (nâng cao).

Ví dụ workflow:

bash
node --trace-opt --trace-deopt --allow-natives-syntax benchmark.js

%GetOptimizationStatus(fn) return values:

  • 1: Optimized by TurboFan
  • 2: Not optimized
  • 3: Always optimized (không dùng nữa)
  • 4: Optimized by Maglev
  • 6: Optimized by TurboFan, maybe deopted

Self-Check: Viết script kiểm tra status của một function trước và sau khi gọi 10000 lần.

E. Code Patterns Ảnh Hưởng Optimization

Pattern 1: Class field arrow function

js
class Counter {
  count = 0;
  increment = () => { this.count++; }; // Arrow function tạo mỗi instance
}

Mỗi instance tạo một function object mới → không share hidden class cho method. Tuy nhiên, V8 hiện đại (v10+) optimize pattern này khá tốt. Trade-off: memory vs convenience.

Pattern 2: Arguments object

js
function leakyArguments() {
  return arguments; // Không dùng arguments trong optimized code
}

arguments object tạo allocation và ngăn certain optimizations. Dùng rest parameter ...args thay thế.

Pattern 3: try-catch trong function

js
function hotPath() {
  try {
    // ...
  } catch (e) {
    // ...
  }
}

TurboFan historically khó optimize function có try-catch. Tách try-catch ra wrapper function:

js
function hotPath() {
  return tryCatchWrapper();
}
function tryCatchWrapper() {
  try { /* ... */ } catch (e) { /* ... */ }
}

4. Mental Model

Model: V8 là nhà máy ô tô với 4 dây chuyền

  • Ignition: Dây chuyền lắp ráp thủ công — kỹ sư đọc bản vẽ (bytecode) và lắp từng bước. Chậm nhưng linh hoạt, làm được mọi loại xe.
  • Sparkplug: Dây chuyền bán tự động — lắp nhanh hơn nhưng không tối ưu chi tiết.
  • Maglev: Dây chuyền tự động — nhận diện được "xe này thường là sedan" và lắp nhanh hơn.
  • TurboFan: Dây chuyền AI — quan sát 1000 chiếc sedan rồi tạo một robot chuyên lắp sedan. Cực nhanh. Nhưng nếu đột nhiên có xe tải đến → robot bối rối → phải gọi kỹ sư thủ công (deoptimize).

Giới hạn: Model không thể hiện việc Maglev và TurboFan có thể cùng tồn tại cho cùng một function (deopt từ TF về Maglev, không nhất thiết về Ignition).

Model: Hidden Class là form đăng ký xe

  • Mỗi object là một chiếc xe. Hidden class là form đăng ký mô tả "xe này có gì" (property, offset).
  • Nếu mọi xe cùng model (C0 → C1 → C2), nhà máy lắp nhanh vì biết chính xác vị trí mỗi bộ phận (monomorphic IC).
  • Nếu xe tùy chỉnh lung tung → mỗi xe một form khác nhau → nhà máy phải đọc form từng chiếc (megamorphic).
  • Nếu xóa bộ phận (delete) → form bị rách → phải dùng sổ tay tra cứu (dictionary mode).

5. Visualization

A. Hidden Class Transition Tree

{} ──► {x} ──► {x,y} ──► {x,y,z}
C0     C1      C2        C3

 └──► {x,a} ──► {x,a,b}
      C4        C5

Nếu delete x từ C2:
{x,y} ──► [Dictionary Mode] (không còn transition)

B. Inline Cache State Machine

Access obj.x


┌─────────────┐
│  Uninitialized │──► Lần đầu: ghi nhận map, chuyển Monomorphic
└─────────────┘


┌─────────────┐
│ Monomorphic │──► Map khớp?──► Yes: fast path (offset known)
│   (1 map)   │           └──► No: chuyển Polymorphic
└─────────────┘


┌─────────────┐
│ Polymorphic │──► Map trong list 2–4?──► Yes: switch nhỏ
│  (2–4 maps) │                           No: chuyển Megamorphic
└─────────────┘


┌─────────────┐
│ Megamorphic │──► Bỏ cache, lookup chậm
│  (>4 maps)  │
└─────────────┘

C. Deoptimization Timeline

Time →
│  Ignition  │  Sparkplug  │   Maglev   │  TurboFan  │
│  ████████  │  ████████   │  ████████  │  ████████  │
│  (cold)    │  (warm)     │  (hot)     │  (very hot)│
│            │             │            │            │
│            │             │            │  Deopt!    │
│            │             │            │  (wrong    │
│            │             │            │   map)     │
│            │             │  ████████  │            │
│            │             │  (fallback)│            │

6. Guided Example

Bài toán: Xác định tại sao function bị deoptimize.

js
// deopt-demo.js
function processPoint(p) {
  return p.x + p.y;
}

// Warmup
for (let i = 0; i < 100000; i++) {
  processPoint({ x: 1, y: 2 });
}

// Deopt trigger
processPoint({ x: 1, y: 2, z: 3 }); // Different shape!

// Continue
for (let i = 0; i < 100000; i++) {
  processPoint({ x: 1, y: 2 });
}

Think Aloud:

  1. Chạy: node --trace-opt --trace-deopt deopt-demo.js
  2. Output sẽ hiện [deoptimizing (DEOPT soft): begin ... processPoint]
  3. Reason: wrong map — hidden class của {x,y,z} khác {x,y}.
  4. Fix: Đảm bảo mọi object truyền vào processPoint có cùng shape, hoặc chấp nhận polymorphic nếu chỉ 2–3 shapes.

Benchmark sau fix:

js
// GOOD: Consistent shape
const template = { x: 0, y: 0 };
function createPoint(x, y) {
  const p = { x, y }; // Cùng C0→C1 transition
  return p;
}

7. Guided Questions

  • Tại sao delete obj.key chuyển object sang dictionary mode? Vẽ transition tree.
  • Nếu một function nhận 5 loại object khác nhau, IC ở trạng thái gì? Tốc độ so với monomorphic?
  • %GetOptimizationStatus trả về 1 nghĩa là gì? Trả về 2 nghĩa là gì?
  • Tại sao try-catch trong hot path historically gây khó optimize? Cách refactor?

8. Guided Practice

Bài tập: Viết script v8-profile.js:

js
function compute(data) {
  let sum = 0;
  for (let i = 0; i < data.length; i++) {
    sum += data[i].value;
  }
  return sum;
}

// Warmup
const consistent = Array.from({ length: 10000 }, () => ({ value: 1 }));
for (let i = 0; i < 5; i++) compute(consistent);

// Test 1: Consistent shape
console.log('Status after consistent:', %GetOptimizationStatus(compute));

// Test 2: Inconsistent shape
const inconsistent = [
  ...Array(5000).fill({ value: 1 }),
  ...Array(5000).fill({ val: 1 }), // different key!
];
compute(inconsistent);
console.log('Status after inconsistent:', %GetOptimizationStatus(compute));

Yêu cầu:

  • [ ] Chạy với node --allow-natives-syntax --trace-opt --trace-deopt v8-profile.js.
  • [ ] Giải thích output: function có bị deoptimize không? Nếu có, reason là gì?
  • [ ] Sửa inconsistent để giữ shape consistent (thêm value: undefined nếu cần).
  • [ ] Chạy lại và xác nhận status = 1 (optimized).

9. Independent Practice

Bài tập 1 (IC Benchmark):
Viết 3 function đọc property x từ array 1 triệu item:

  • mono: Tất cả item cùng shape {x, y}.
  • poly: 3 shapes khác nhau (nhưng cùng key x).
  • mega: 10 shapes khác nhau.

Benchmark bằng performance.now(). Ghi nhận tỷ lệ chênh lệch.

Bài tập 2 (Deopt Investigation):
Tìm một đoạn code trong project thực (hoặc viết mô phỏng) có thể gây deoptimization. Dùng --trace-deopt để xác nhận. Viết ADR gồm: context, hypothesis, verification, fix.


10. Edge Cases

A. evalwith vô hiệu hóa toàn bộ optimization trong scope

js
function hotPath(obj) {
  with (obj) { // V8 bỏ optimize toàn bộ function này
    return x + y;
  }
}

Thực tế: Code legacy dùng with để tránh viết dài. Refactor bằng destructuring hoặc explicit property access.

B. arguments object trong strict mode vs non-strict

js
function leaky(a) {
  arguments[0] = 10; // Non-strict: alias với `a`. Strict: không alias.
  return a;
}

Thực tế: Dùng ...args để tránh allocation và cho phép optimization.

C. Hidden class transition "tree" bị phân mảnh

js
// Tạo 1000 object với thứ tự property khác nhau
for (let i = 0; i < 1000; i++) {
  if (i % 2 === 0) {
    objs.push({ a: 1, b: 2 });
  } else {
    objs.push({ b: 2, a: 1 }); // Different transition path!
  }
}

Thực tế: JSON parser hoặc API response có thứ tự key không deterministic → hidden class fragmentation. Fix: Normalize object sau parse (dùng hàm tạo object với thứ tự cố định).


11. Reflection

Teach Back:
Giải thích cho senior developer: "Tại sao đoạn code của tôi bị deoptimize?" Dùng output từ --trace-deopt, hidden class transition tree, và IC state machine.

Self-Check:

  • [ ] Tôi chạy được node --trace-opt --trace-deopt.
  • [ ] Tôi đọc được %GetOptimizationStatus.
  • [ ] Tôi giải thích được monomorphic vs polymorphic vs megamorphic.
  • [ ] Tôi biết tại sao deletewith phá optimization.

12. Exit Exam

Bài thi:

  1. Profile & Fix: Cho một function chậm trong codebase. Dùng --trace-opt --trace-deopt xác định lý do. Fix bằng cách giữ shape consistent hoặc refactor try-catch. Chứng minh bằng benchmark trước/sau.
  2. IC Analysis: Viết 3 function (mono/poly/mega). Benchmark và giải thích kết quả dựa trên IC theory. Dùng %HaveSameMap để verify.
  3. ADR: Viết Architecture Decision Record cho việc "Có nên dùng class field arrow function trong hot path không?" Cân nhắc memory, optimization, DX. Đưa ra quyết định có dữ kiện.

Pass criteria:

  • Đọc được --trace-deopt output và xác định đúng reason.
  • Benchmark chứng minh monomorphic nhanh hơn megamorphic ít nhất 3x.
  • ADR có ít nhất 2 lựa chọn, mỗi lựa chọn có rủi ro cụ thể.

STAGE 0.1 CHECKPOINT

Trước khi sang Module 0.1b hoặc 0.1a, tự trả lời:

Câu hỏiNếu "Không" → Quay lại
Tôi vẽ được Event Loop trên giấy?Module 0.1 (gốc)
Tôi giải thích được V8 pipeline 4 tier?Module 0.1 (gốc)
Tôi biết tại sao object shape consistency quan trọng?Module 0.1 (gốc)
Tôi chụp được 3 snapshot tìm leak?Module 0.1b
Tôi dùng LoAF API đo INP?Module 0.1b
Tôi đọc được --trace-deopt output?Module 0.1a
Tôi benchmark được mono/poly/mega IC?Module 0.1a

Quy tắc: Không sang Stage 1 nếu chưa đạt Exit Criteria của tất cả module Stage 0.

Không timeline cứng. Chỉ có thứ tự module và Exit Criteria.