Skip to content

Module 0.1.1 — V8 Engine & Memory Basics

Nhiều lập trình viên được dạy rằng "primitive nằm ở stack, object nằm ở heap" — như thể đó là quy tắc bất di bất dịch của JavaScript. Nếu quy tắc đó đúng, tại sao một closure nhỏ bé có thể giữ một giá trị sống sót qua hàng trăm lần gọi hàm? Trong module này, chúng ta sẽ tháo gỡ nhận thức cũ và xây dựng lại từ spec behavior: lifetime quyết định bởi reachability, không phải bởi kiểu dữ liệu.

1. Object Lifetime: Reachability Quyết Định Tất Cả

Hầu hết chúng ta được dạy "biến local biến mất khi hàm return." Điều đó đúng — cho đến khi có closure. Chạy thử trước khi đọc tiếp:

js
function createCounter() {
  let count = 0;
  return {
    increment() { return ++count; },
    get() { return count; }
  };
}

const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.get());      // 2
🤔 Think First (Tự suy luận trước)

createCounter đã return. Theo nhận thức cũ, count nên biến mất. Nhưng counter.get() vẫn trả về 2.

Điều gì giữ count tồn tại?

✅ Reveal (Đáp án & giải thích)

Đúng — count vẫn reachable qua closure. Execution context của createCounter đã kết thúc, nhưng binding count vẫn được tham chiếu bởi hàm bên trong object trả về.

Spec behavior: Lifetime của một binding phụ thuộc vào reachability, không phải vào việc nó là primitive hay object, và không phải vào việc function đã return hay chưa.

Tại Sao "Stack vs Heap" Là Cách Nói Thiếu Chính Xác?

⚠️ Anti-Pattern

Nói "primitive ở stack, object ở heap"implementation heuristic, không phải spec behavior. Nó tạo ra hai hậu quả nguy hiểm:

  1. Nghĩ rằng primitive không thể "leak" memory.
  2. Nghĩ rằng object luôn sống lâu hơn primitive.

Thực tế: một primitive trong closure có thể sống lâu như bất kỳ object nào — miễn là nó còn reachable.

🧠 Staff Insight (Góc nhìn ở cấp Staff)

Khi review code, đừng hỏi "Biến này là primitive hay object?" Hãy hỏi: "Binding này còn reachable không? Và từ đâu?"

🧪 Experiment: Lifetime Under Pressure

🧪 Experiment

Step 1 — Baseline: Chạy đoạn code sau. Dự đoán: count có bị GC sau dòng return không?

js
function factory() {
  let count = 0;
  const obj = { value: count };
  return obj;
}

const result = factory();
// `count` còn reachable không?

Step 2 — Change: Thay return obj bằng return () => count.

Step 3 — Observe: Trong cả hai trường hợp, count có thể bị GC khi nào?

Step 4 — Conclusion: Lifetime phụ thuộc vào reachability graph, không phụ thuộc vào kiểu dữ liệu.


2. Execution Context & Dynamic Memory

Bạn vừa thấy object có thể sống lâu hơn function tạo ra nó nhờ closure. Nhưng object không phải thứ duy nhất chiếm bộ nhớ — phần tiếp theo sẽ cho bạn thấy hai vùng nhớ chính mà engine quản lý, và tại sao cách phân biệt "stack vs heap" đã lỗi thời.

Mental Model Đúng: Hai Vùng Quản Lý Khác Nhau

Thay vì nói "stack" và "heap", hãy nghĩ về:

  1. Execution Context Chain — LIFO, quản lý bởi call stack. Mỗi lần gọi hàm tạo một context mới. Quá sâu → stack overflow.
  2. Dynamically-Managed Memory — Vùng nhớ engine quản lý để lưu object, closure environment, và các cấu trúc phức tạp. Quá nhiều → memory pressure.
js
// Execution context chain tăng khi đệ quy sâu
function recurse(n) {
  if (n === 0) return 0;
  return recurse(n - 1) + 1;
}
recurse(100000); // RangeError: Maximum call stack size exceeded

// Dynamically-managed memory tăng khi object lớn
const huge = {};
for (let i = 0; i < 1e6; i++) {
  huge[`key-${i}`] = i;
}
// Không lỗi stack, nhưng memory usage tăng
🤔 Socratic Challenge: Closure Promotion

Anchor: Bạn vừa thấy count trong closure vẫn sống sau khi hàm return.

Challenge: Nếu "mọi thứ trong execution context biến mất khi return" đúng, tại sao counter.increment() vẫn truy cập được count?

Probe: Nếu ta gán counter = null, điều gì xảy ra với count?

Synthesize: Vậy điều kiện thực sự quyết định lifetime là gì?

Observable: Closure Promotion

Khi một binding được capture bởi closure, lifetime của nó kéo dài vì vẫn reachable:

js
function createHeavyData() {
  const bigArray = new Array(1e6).fill('x');
  const smallValue = 42;
  
  return function getValue() {
    return smallValue; // bigArray có được giữ lại không?
  };
}

const getValue = createHeavyData();
🤔 Think First (Tự suy luận trước)

getValue chỉ truy cập smallValue. bigArray có bị GC không?

✅ Reveal

Có thể — nhưng không đảm bảo tức thì. Engine có thể nhận diện rằng bigArray không còn reachable, nhưng trong một số implementation, cả execution context có thể được giữ lại. Đây là lý do chúng ta cần cẩn thận với closure capture.

Best practice: Nếu chỉ cần một phần nhỏ dữ liệu, chỉ capture phần đó.

js
// ✅ Chỉ capture những gì cần
function createLightClosure() {
  const bigArray = new Array(1e6).fill('x');
  const result = bigArray[0];
  return () => result; // Không tham chiếu bigArray
}

3. Allocation Pressure: Khi Tạo Object Trở Thành Vấn Đề

Hiểu về execution context và dynamic memory là nền tảng. Nhưng trong production, vấn đề không phải "ở đâu" mà là "tạo ra bao nhiêu". Khi bạn tạo object quá nhanh, điều gì xảy ra?

Observable: Sawtooth Pattern

Mở Chrome DevTools → Performance → Memory. Chạy đoạn code:

js
// ❌ BAD: Allocation pressure cao
function updateDashboardBad(data) {
  return data.map(point => ({
    x: point.x,
    y: point.y,
    label: `Point-${point.id}`,
    timestamp: Date.now()
  }));
}

// Giả lập 60fps update
setInterval(() => {
  const data = Array.from({ length: 1000 }, (_, i) => ({ x: i, y: Math.random(), id: i }));
  const processed = updateDashboardBad(data);
  // ...render
}, 16);

Bạn sẽ thấy sawtooth pattern: memory tăng liên tục, rồi đột ngột giảm khi GC chạy.

🧪 Experiment: Allocation Pressure

Step 1 — Baseline: Chạy đoạn BAD trên trong DevTools Performance 5 giây. Ghi lại: frame time trung bình, số lần GC chạy.

Step 2 — Change: Tái sử dụng object thay vì tạo mới mỗi frame.

js
// ✅ GOOD: Tái sử dụng
const pool = Array.from({ length: 1000 }, () => ({ x: 0, y: 0, label: '', timestamp: 0 }));

function updateDashboardGood(data) {
  for (let i = 0; i < data.length; i++) {
    const p = pool[i];
    p.x = data[i].x;
    p.y = data[i].y;
    p.label = `Point-${data[i].id}`;
    p.timestamp = Date.now();
  }
  return pool;
}

Step 3 — Observe: Chạy lại 5 giây. So sánh: sawtooth có giảm không? Frame time có ổn định hơn không?

Step 4 — Conclusion: Nếu sawtooth giảm → allocation pressure là bottleneck.

Mechanism: Tại Sao Allocation Pressure Đau?

Mỗi lần tạo object mới:

  1. Engine cấp phát vùng nhớ trong dynamically-managed memory.
  2. Khi vùng nhớ đầy, GC phải chạy để dọn rác.
  3. GC pause làm gián đoạn main thread → frame drop, jank.

🧠 Staff Insight

Trong hot path (animation, real-time update, game loop), allocation pressure là kẻ thù số một. Không phải "object lớn", mà là "object tạo liên tục".


4. Hidden Class: Shape Descriptor

Giảm allocation pressure là cách phòng thủ. Nhưng còn một yếu tố khác quyết định performance: hình dạng của object.

Mental Model

Trong V8, mỗi object không lưu trực tiếp danh sách property. Thay vào đó, nó có một shape descriptor (thường gọi là hidden class) — giống như khuôn mẫu mô tả: "object này có property gì, nằm ở offset nào."

js
const user = { name: 'An', age: 25, role: 'admin' };
// V8 tạo một shape: { name, age, role }

Khi bạn truy cập user.name, V8 không tìm kiếm linear. Nó nhìn vào shape và biết: "name nằm ở offset 0."

Learning Goal

Đây là knowledge bridge. Bạn không cần biết cách V8 cài đặt shape transition tree — chỉ cần hiểu rằng object có một "khuôn mẫu" và khuôn mẫu này ảnh hưởng tốc độ truy cập.


5. Shape Stability: Quyết Định Layout Object

Hidden class là nền tảng. Nhưng nền tảng này chỉ có ích nếu bạn giữ cho object có hình dạng ổn định. Phần tiếp theo sẽ cho bạn thấy tại sao "thêm property sau khi tạo" là một quyết định có hậu quả.

Observable: Stable vs Unstable

js
function createUserUnstable(name) {
  const user = { name };           // Shape 1: { name }
  user.age = 25;                   // Shape 2: { name, age }
  user.role = 'admin';             // Shape 3: { name, age, role }
  return user;
}
js
function createUserStable(name) {
  // Shape 1 duy nhất: { name, age, role }
  return { name, age: 25, role: 'admin' };
}
🤔 Think First (Tự suy luận trước)

Nếu tạo 1 triệu user bằng cả hai cách, cách nào nhanh hơn? Và quan trọng hơn: tại sao?

✅ Reveal

Stable shape nhanh hơn vì:

  1. Chỉ tạo một shape descriptor thay vì ba.
  2. Mọi instance chia sẻ cùng một shape → cache hiệu quả hơn.
  3. Engine có thể dự đoán vị trí property.

🧪 Experiment (Thử nghiệm & quan sát): Shape Instability

🧪 Experiment

Step 1 — Baseline: Chạy 1 triệu lần createUserUnstable.

Step 2 — Change: Chạy 1 triệu lần createUserStable.

Step 3 — Observe: So sánh thời gian trong DevTools Console.

Step 4 — Conclusion: Sự khác biệt có đáng kể không? Trong hot path, shape instability là bottleneck ẩn.

Edge Case Lab

🧪 Edge Case Lab #1: Conditional Property

js
function createUserConditional(name, isAdmin) {
  const user = { name };
  if (isAdmin) {
    user.role = 'admin';  // Shape thay đổi tùy đường chạy!
  }
  return user;
}

Hypothesis: Object tạo ra có cùng shape không?

Observation: Không. Một số có shape { name }, một số có { name, role }polymorphic.

Fix: Luôn khởi tạo cùng một tập property, dùng undefined nếu cần.

js
// ✅ Shape ổn định
return { name, role: isAdmin ? 'admin' : undefined };

🧪 Edge Case Lab #2: Dynamic Keys

js
const config = {};
for (let i = 0; i < 100; i++) {
  config[`key-${i}`] = i;  // Mỗi lần thêm property = shape mới?
}

Hypothesis: Object này có bao nhiêu shape transitions?

Observation: Nhiều. V8 có thể chuyển sang dictionary mode khi property quá nhiều hoặc quá động.

Fix: Nếu key động và số lượng lớn, cân nhắc Map thay vì object.

🧪 Edge Case Lab #3: Property Delete

js
const user = { name: 'An', age: 25, role: 'admin' };
delete user.age;

Hypothesis: delete có ảnh hưởng gì đến shape không?

Observation: Trong hot path, delete có thể làm representation kém predictable hơn. Shape có thể không còn ổn định.

Fix: Đặt undefined thay vì delete nếu cần giữ shape.

🏗️ Architectural Edge Case: API Response Normalization

System Context: Backend trả response với field order không đảm bảo.

json
// Response A
{ "id": 1, "name": "An", "email": "an@example.com" }

// Response B  
{ "email": "be@example.com", "id": 2, "name": "Be" }

Assumption: Object tạo từ JSON luôn có cùng shape.

The Break: Field order khác nhau → V8 có thể tạo shape khác nhau cho mỗi response.

Design Question: Nếu bạn normalize 10K API response mỗi giây, làm sao đảm bảo shape stability?

Trade-off Table:

ApproachProsConsWhen to Use
Normalize bằng hàm tạo object cố địnhShape ổn định, IC hitThêm codeHot path, high frequency
Dùng Map cho dữ liệu độngKhông lo shapeKhông có IC optimizationKey động, số lượng lớn
Schema validation (Zod)Shape ổn định + type safeRuntime costBoundary layer

Production Context: Shape Stability Policy

Trong team, bạn nên có một policy đơn giản:

  1. Khởi tạo đầy đủ property ngay khi tạo object.
  2. Không thêm property sau khởi tạo trong hot path.
  3. Không dùng delete trong hot path.
  4. Dùng Map nếu key động hoặc số lượng property lớn.

🧠 Staff Insight

Khi review production code, đừng chỉ hỏi "Code này có chạy không?" Hãy kiểm tra thêm:

  1. Object shape có ổn định không?
  2. Hot path có allocation không cần thiết không?
  3. Property access có gặp quá nhiều shapes không?

6. Inline Cache: Hậu Quả Của Shape Stability

Shape stability tạo điều kiện cho một cơ chế tối ưu khác. Nếu site truy cập luôn thấy cùng một shape, engine có thể làm một điều thông minh.

Mental Model

Mỗi lần code chạy user.name, V8 ghi nhớ: "Tại địa chỉ code này, object thường có shape X, và name nằm ở offset 0." Lần sau gặp cùng shape, không cần lookup — lấy offset trực tiếp.

Đây là inline cache — caching tại mỗi site truy cập.

Trạng tháiÝ nghĩaHậu quả
Monomorphic1 shape duy nhấtNhanh nhất — cache hit
Polymorphic2-4 shapes khác nhauVẫn cache được, nhưng chậm hơn
Megamorphic>4 shapes hoặc quá độngCache bỏ cuộc → slow path
🤔 Think First (Tự suy luận trước)

Đoạn code này là monomorphic hay megamorphic?

js
function getName(user) {
  return user.name;
}

getName({ name: 'An', age: 25 });
getName({ name: 'Be', role: 'admin' });
✅ Reveal

Megamorphic — hai object có shape khác nhau ({ name, age } vs { name, role }). Inline cache tại getName phải xử lý nhiều shapes.

Fix: Chuẩn hóa shape:

js
// ✅ Cùng shape { name, age, role }
getName({ name: 'An', age: 25, role: undefined });
getName({ name: 'Be', age: undefined, role: 'admin' });

7. Generational GC: Young vs Old

Chúng ta đã nói về object layout và access pattern. Nhưng object không tồn tại mãi mãi — chúng cần được dọn dẹp. V8 dọn dẹp như thế nào?

Mental Model

V8 chia dynamically-managed memory thành hai vùng chính:

  1. Young Generation — Object mới tạo. Đa số object chết nhanh (temporary).

    • Scavenge: Thu gom nhanh, chỉ quét Young. Tốn ít thời gian.
  2. Old Generation — Object sống đủ lâu (được promote từ Young).

    • Mark-Sweep-Compact: Thu gom toàn bộ, chậm hơn, nhưng hiếm.

Learning Goal

Bạn không cần biết write barrier hay incremental GC internals. Chỉ cần nhớ: object mới chết nhanh → Scavenge nhanh. Object sống lâu → Mark-Sweep chậm hơn.

🤔 Think First (Tự suy luận trước)

Tại sao object "sống đủ lâu" lại được promote sang Old Generation? Có phải vì chúng "quan trọng hơn"?

✅ Reveal

Không phải vì quan trọng, mà vì chi phí quét lặp đi lặp lại object sống lâu là lãng phí. Nếu một object đã sống sót qua 2 lần Scavenge, khả năng cao nó sẽ tiếp tục sống. Đưa nó sang Old để Young Generation nhỏ gọn → Scavenge nhanh hơn.


8. Decision Record: Chiến Lược Bộ Nhớ Dashboard Real-time

Giờ bạn đã có đủ building block: lifetime, allocation pressure, shape stability, và GC. Hãy áp dụng chúng vào một quyết định thực tế.

Context

Bạn đang xây dựng real-time dashboard hiển thị 1000 data points, cập nhật mỗi 100ms. Mỗi lần cập nhật tạo 1000 object mới → allocation pressure cực cao, sawtooth pattern nghiêm trọng.

Constraint: Frame time phải < 16ms. Không được để GC pause gây jank.

Option A: Ring Buffer

js
class RingBuffer {
  constructor(size) {
    this.buf = Array.from({ length: size }, () => ({ x: 0, y: 0 }));
    this.i = 0;
  }
  push(x, y) {
    const p = this.buf[this.i];
    p.x = x; p.y = y;
    this.i = (this.i + 1) % this.buf.length;
  }
  getAll() {
    return this.buf.slice(this.i).concat(this.buf.slice(0, this.i));
  }
}
  • Ưu điểm: Pre-allocate một lần. Zero allocation sau init. Shape ổn định.
  • Nhược điểm: Fixed size, mất lịch sử cũ khi đầy.

Option B: Object Pool

js
class ObjectPool {
  constructor(initialSize = 1000) {
    this.available = Array.from({ length: initialSize }, () => ({ x: 0, y: 0, inUse: false }));
    this.active = [];
  }
  acquire() {
    let obj = this.available.find(o => !o.inUse);
    if (!obj) {
      obj = { x: 0, y: 0, inUse: true }; // Grow khi cần
      this.available.push(obj);
    }
    obj.inUse = true;
    this.active.push(obj);
    return obj;
  }
  releaseAll() {
    this.active.forEach(o => o.inUse = false);
    this.active.length = 0;
  }
}
  • Ưu điểm: Tái sử dụng object. Có thể grow khi cần. Shape ổn định.
  • Nhược điểm: Management complexity. find() trong acquire() có thể chậm nếu pool lớn.

Decision

🎯 Decision

Chọn Ring Buffer nếu:

  • Window dữ liệu cố định (ví dụ: "1000 điểm gần nhất").
  • Không cần lịch sử đầy đủ.
  • Ưu tiên zero allocation và đơn giản.

Chọn Object Pool nếu:

  • Số lượng object biến động.
  • Cần giữ lịch sử và có thể grow.
  • Chấp nhận complexity để có flexibility.

Với context dashboard real-time 1000 điểm cố định: Ring Buffer phù hợp hơn vì constraint chính là allocation pressure, không phải kích thước biến động.


9. 🔗 Connected Preview (Kết nối kiến thức liên quan): TypedArray & Struct-of-Arrays

Trước khi kết thúc module, có ba khái niệm nữa bạn nên biết tên — chúng sẽ trở nên quan trọng ở các stage sau.

🔗 Connected Preview: TypedArray

What: Array kiểu số (Uint8Array, Float64Array...) — contiguous memory.

Why: Memory efficiency + performance cho số liệu lớn.

Connection: Vừa học memory layout — TypedArray khác object thường ở contiguous allocation, không có hidden class overhead cho từng phần tử.

Future Payoff: Module 4.3.2 (Web Workers + xử lý số liệu lớn).

Deep Dive Trigger: 4.3.2

You Don't Need This Now: true

Dependency Status: Prerequisite=NO | CurrentUse=NO | FutureValue=MEDIUM


10. 🔭 Future Preview (Nhìn trước kiến thức sắp học): JIT Pipeline & Pointer Compression

🔭 Future Preview: JIT Pipeline

What: V8 biên dịch JavaScript qua nhiều tầng — Ignition (interpreter) → Sparkplug → Maglev → TurboFan (optimizing compiler).

Why: Code lúc nhanh lúc chậm vì V8 "học" từ behavior thực tế. Ban đầu chậm (interpreter), sau tối ưu (compiler) nếu hàm chạy nhiều lần.

Future Payoff: Nếu bạn làm performance engineering hoặc debug deoptimization ở Stage 4+.

You Don't Need This Now: true.

🔭 Future Preview: Pointer Compression

What: V8 nén pointer 64-bit xuống 32-bit để tiết kiệm memory.

Why: Giảm heap size ~40% trên 64-bit system.

Future Payoff: Nếu bạn làm V8 performance engineering.

You Don't Need This Now: true.


11. ♻️ Reflection: Teach Back

♻️ Reflection

Prompt: Giả sử bạn phải dạy "Object Lifetime" cho một Junior vừa vào team. Bạn sẽ giải thích như thế nào trong 2 phút?

Analogy Check: Analogy bạn chọn là gì? (Ví dụ: "object như người thuê nhà, GC là chủ nhà dọn dẹp khi không còn ai thuê.") Nó có break ở điểm nào không?

Misconception Guard: Junior thường hiểu nhầm "primitive ở stack, object ở heap" thành quy tắc bất biến. Bạn sẽ phòng tránh bằng cách nào?

Self-Check:

  • [ ] Tôi có thể giải thích object lifetime mà không nhìn tài liệu
  • [ ] Tôi biết reachability khác "stack vs heap" ở điểm nào
  • [ ] Tôi có thể chỉ ra 1 anti-pattern liên quan đến shape instability
  • [ ] Tôi có thể trả lời "Tại sao không dùng object pool cho mọi thứ?" trong 30 giây

12. 🎤 Interview Q&A: Memory Layout Strategy

🎤 Interview Q&A

Question: "You have 100k user sessions in memory. Design a memory layout strategy considering hidden classes, inline caching, and GC pressure. When would you choose struct-of-arrays over array-of-structs? What metrics do you track?"

L1 — Concept (30s):

Memory layout strategy for frontend involves three pillars: shape stability for IC hit rate, allocation pressure reduction for GC health, and reachability management for leak prevention. Struct-of-arrays (SoA) stores each field in separate arrays, while array-of-structs (AoS) stores objects with mixed fields.

Bản dịch — L1

Chiến lược bố trí bộ nhớ frontend dựa trên ba trụ cột: shape stability để IC hit rate cao, giảm allocation pressure để GC khỏe, và quản lý reachability để tránh leak. Struct-of-arrays (SoA) lưu từng field trong mảng riêng biệt, còn array-of-structs (AoS) lưu object có nhiều field hỗn hợp.

L2 — Application (1m):

For 100k sessions, I'd first audit shape stability: ensure all session objects are created with identical property order. If sessions have dynamic fields (e.g., optional cart, preferences), I'd normalize to a fixed schema with undefined defaults. For GC pressure, I'd use object pooling if sessions are short-lived and high-churn, or a ring buffer if we only need the latest N sessions.

Bản dịch — L2

Với 100k session, tôi sẽ audit shape stability trước: đảm bảo mọi session object được tạo với cùng property order. Nếu session có field động (ví dụ: cart, preferences tùy chọn), tôi normalize về schema cố định với default undefined. Về GC pressure, tôi dùng object pool nếu session ngắn hạn và thay đổi nhanh, hoặc ring buffer nếu chỉ cần N session gần nhất.

L3 — Deep Dive (2-3m):

SoA vs AoS trade-off: AoS (array of session objects) gives better code readability and natural JS patterns, but each session object has its own hidden class overhead and scatters memory. SoA (separate sessionIds[], sessionTimestamps[], sessionStatuses[]) improves cache locality, eliminates per-object header overhead, and guarantees monomorphic access patterns. I'd choose SoA when:

  1. Homogeneous data with frequent bulk operations (filter all expired sessions)
  2. Need to pass data to Web Worker or WebGL without serialization cost
  3. Memory is constrained (mobile devices)

Metrics I'd track: heap snapshot size, Scavenge frequency (DevTools Performance), IC state via %HasFastProperties or benchmark comparison, and frame time consistency during session updates.

Bản dịch — L3

Trade-off SoA vs AoS: AoS (mảng session object) dễ đọc và tự nhiên với JS, nhưng mỗi object có hidden class overhead và phân tán bộ nhớ. SoA (mảng sessionIds[], sessionTimestamps[], sessionStatuses[] riêng biệt) cải thiện cache locality, loại bỏ overhead header từng object, và đảm bảo monomorphic access. Tôi chọn SoA khi:

  1. Dữ liệu đồng nhất và cần bulk operation thường xuyên (lọc session hết hạn)
  2. Cần truyền data sang Web Worker hoặc WebGL không cần serialize
  3. Bộ nhớ hạn chế (thiết bị di động)

Metric tôi track: heap snapshot size, tần suất Scavenge (DevTools Performance), IC state qua benchmark comparison, và frame time consistency khi update session.

L4 — System Design (5m):

Requirements: 100k concurrent sessions, real-time updates every 5s, mobile-first (2GB RAM constraint), 60fps UI.

Constraints: Browser heap limit (~1.5GB on mobile), main thread must not GC jank, session data must survive page refresh (optional).

Approach A — AoS + Normalization: Standard JS objects in a Map by sessionId. Simple, but 100k objects = 100k hidden classes + headers ≈ 20MB overhead. GC Scavenge every few seconds.

Approach B — SoA + TypedArray: Int32Array for IDs, Float64Array for timestamps, Uint8Array for status codes. Single contiguous block ~2.5MB. Zero hidden class overhead. Monomorphic access. Bulk filter via typed array slice.

Trade-off: I choose hybrid — SoA for hot path (active sessions list rendering), AoS for individual session detail view (rarely accessed, needs flexibility). This accepts the complexity of maintaining two representations in exchange for rendering performance.

Failure Mode: If session count grows to 1M, SoA may exceed contiguous allocation limit. Fallback: shard into chunks of 50k sessions.

Monitoring: RUM heap size, performance.memory.usedJSHeapSize, INP during session updates, CrUX field data.

Cost: SoA implementation adds ~2 days dev time, saves ~15MB per user → on 2GB device, reduces OOM probability significantly.

Bản dịch — L4

Requirements: 100k session đồng thời, cập nhật real-time mỗi 5s, mobile-first (RAM 2GB), UI 60fps.

Constraints: Browser heap limit (~1.5GB mobile), main thread không được bị GC jank, session data có thể cần survive refresh.

Approach A — AoS + Normalization: Object JS thường trong Map theo sessionId. Đơn giản, nhưng 100k object = 100k hidden class + header ≈ 20MB overhead. GC Scavenge mỗi vài giây.

Approach B — SoA + TypedArray: Int32Array cho ID, Float64Array cho timestamp, Uint8Array cho status. Một block contiguous ~2.5MB. Không hidden class overhead. Access monomorphic. Filter bulk qua typed array slice.

Trade-off: Tôi chọn hybrid — SoA cho hot path (render danh sách session active), AoS cho detail view từng session (truy cập hiếm, cần linh hoạt). Chấp nhận complexity duy trì hai representation để đổi lấy rendering performance.

Failure Mode: Nếu session tăng lên 1M, SoA có thể vượt contiguous allocation limit. Fallback: shard thành chunk 50k session.

Monitoring: RUM heap size, performance.memory.usedJSHeapSize, INP khi update session, CrUX field data.

Cost: Implement SoA thêm ~2 ngày dev, tiết kiệm ~15MB mỗi user → trên thiết bị 2GB, giảm đáng kể xác suất OOM.

:::


Module Summary

Concepts Taught

  • Object Lifetime — Reachability quyết định lifetime, không phải kiểu dữ liệu hay vị trí lưu trữ. Closure giữ binding sống sót qua execution context.
  • Execution Context vs Dynamic Memory — Call stack quản lý execution context (LIFO, overflow khi đệ quy sâu). Dynamically-managed memory lưu object và closure environment (pressure khi tạo nhiều object).
  • Allocation Pressure — Sawtooth pattern trong DevTools là dấu hiệu tạo object liên tục. Giải pháp: tái sử dụng object (pool, ring buffer).
  • Hidden Class — Shape descriptor mô tả layout property của object. Không phải implementation detail cần nhớ, mà là nền tảng để hiểu shape stability.
  • Shape Stability — Khởi tạo object với đầy đủ property, không thêm/xóa property sau khởi tạo trong hot path. Quyết định object layout để tối ưu access.
  • Inline Cache — Per-site caching dựa trên shape. Monomorphic = nhanh, megamorphic = chậm. Hậu quả của shape instability.
  • Generational GC — Young generation (Scavenge, nhanh, thường xuyên) vs Old generation (Mark-Sweep-Compact, chậm, hiếm). Object sống đủ lâu được promote.
  • Memory Strategy Decision — Ring buffer (fixed size, zero alloc) vs Object pool (flexible, reusable). Chọn dựa trên constraint: allocation pressure vs size flexibility.

Senior Exit Capability

Sau module này, learner có thể:

  • [ ] Giải thích object lifetime bằng reachability-first model, không dùng "primitive ở stack, object ở heap"
  • [ ] Dự đoán closure behavior và lifetime extension qua reachability graph
  • [ ] Nhận diện allocation pressure từ sawtooth pattern trong DevTools
  • [ ] Quyết định object layout để đảm bảo shape stability trong hot path
  • [ ] Phân biệt monomorphic vs megamorphic access và hậu quả performance
  • [ ] Giải thích Generational GC ở mức concept (Young/Old, Scavenge/Mark-Sweep)
  • [ ] Viết Decision Record cho memory strategy (ring buffer vs object pool) với context và constraint

Anti-Patterns Covered

  • "Primitive ở stack, object ở heap" — Cách nói thiếu chính xác, gây hiểu nhầm về lifetime và leak.
  • Shape Instability — Thêm property sau khởi tạo, conditional property, dynamic keys, delete property trong hot path.
  • Over-allocation in Hot Path — Tạo object mới mỗi frame trong animation/real-time update thay vì tái sử dụng.
  • Closure Over-capture — Capture toàn bộ execution context khi chỉ cần một giá trị nhỏ.

Forward References

  • TypedArray sẽ học ở module 4.3.2 — contiguous memory cho xử lý số liệu lớn trong Web Workers.
  • JIT Pipeline sẽ học ở Stage 4+ nếu làm performance engineering (TurboFan/Maglev internals không cần ở Senior Exit).
  • Pointer Compression là engine-level optimization, không cần cho application debugging — sẽ đề cập nếu làm V8 deep dive.