jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

How Browsers Work · Part 6 — The JavaScript Engine (V8)

Inside modern V8: parsing, Ignition bytecode, Sparkplug and Maglev tiering, TurboFan optimization, Maps, inline caches and evidence-based performance work.

JavaScript trông dynamically typed và “dễ tính” — bạn thêm property bất cứ lúc nào, object có thể đổi shape, còn Number theo spec là IEEE-754 double. Bên dưới trong V8 (Chrome, Edge, Node.js), nhiều execution tier dùng feedback về type/shape để cân bằng startup, chi phí compile và peak performance. Code tối ưu mang guard; khi giả định không còn đúng, engine có thể deoptimize về tier thấp hơn.

Phần này xây mental model để đọc profile, không biến chi tiết V8 thành coding law. Các tier, threshold và representation là implementation detail có thể đổi; API semantics và trace thực tế mới là contract của bạn.

Source JS


 Parser ──► AST


 Ignition (bytecode interpreter) ──► feedback


 Sparkplug (baseline JIT) ──► machine code, little optimization


 Maglev (fast optimizing JIT) ──► speculative optimized code


 TurboFan/Turboshaft (top tier) ──► peak optimized code
    │                                  │
    │◄──── deopt / fall back when guards fail ────┘

 Execute

Các mũi tên là tiering ladder, không phải checklist bắt buộc: function lạnh có thể ở Ignition/Sparkplug, còn code phù hợp mới lên Maglev hoặc top tier.


1. Parsing và AST

Khi V8 nhận source JavaScript, parser dựng Abstract Syntax Tree (AST) — biểu diễn có cấu trúc của cú pháp chương trình. AST là thứ các giai đoạn sau (sinh bytecode, tối ưu) làm việc trên đó.

V8 có thể pre-parse/lazy-parse nhiều function lồng: parser hiện tại dựng AST cho code cần compile, còn body đủ điều kiện có thể chỉ được preparse để ghi metadata và parse đầy đủ khi cần. Không phải function nào cũng luôn lazy — heuristic, eager-compile hint và syntax có thể đổi quyết định.

Hệ quả ổn định hơn chi tiết heuristic: bundle không dùng vẫn tốn download/decompress/scan và một phần parse. Code splitting giảm byte ban đầu và lượng code engine phải chuẩn bị, nhưng phải cân với waterfall/chunk overhead.


2. Ignition: bytecode trước

Sau parse, Ignition sinh bytecode và interpreter thực thi nó. Bytecode gọn, sinh nhanh và là representation chung để tier tiếp theo dùng.

Trong lúc code chạy, V8 thu feedback về call/property access, type và object Map. Tiering manager dùng nhiều tín hiệu — không chỉ một ngưỡng số lần gọi cố định — để quyết định compile tier nào đáng trả chi phí.

Feedback vector gắn với function/call site ghi điều engine đã quan sát; Sparkplug, Maglev và TurboFan có thể dùng hạ tầng đó ở mức khác nhau.


3. Sparkplug và Maglev: hai bậc ở giữa

Pipeline hiện đại không nhảy thẳng Ignition → TurboFan:

  • Sparkplug là baseline compiler: dịch bytecode thành machine code rất nhanh, hầu như không tối ưu nặng. Nó bỏ overhead dispatch của interpreter với compile cost thấp.
  • Maglev là fast optimizing JIT: dùng feedback để sinh code speculative tốt hơn Sparkplug nhưng compile nhanh hơn top tier.

V8 giới thiệu Sparkplug từ v9.1 và Maglev trong Chrome 117; xem bài SparkplugMaglev từ chính V8 team.


4. TurboFan và deoptimization

TurboFan là top-tier optimizing compiler tập trung vào peak performance; Turboshaft là hạ tầng compiler IR/lowering mới bên trong đường top-tier, không phải một API hay tier mà application chọn. Với code đủ nóng và feedback đủ ổn định, compiler có thể sinh machine code chuyên biệt có guard.

Các guard mã hóa giả định:

  • "Biến này luôn là Smi (số nguyên nhỏ)".
  • "Object này luôn có property \x` ở offset 16”`.
  • "Call site này luôn thấy object của hidden class \Map#42".

Code tối ưu chạy khi giả định còn đúng. Khi thực tế lệch — ví dụ call site thấy Map/type chưa được hỗ trợ — guard có thể fail và V8 deoptimize: tái dựng execution state rồi rơi về code ở tier thấp hơn, sau đó có thể tối ưu lại với feedback mới.

Deoptimization là cơ chế đúng đắn, không tự nó là bug. Chỉ khi profile cho thấy compile/deopt lặp hoặc một hot path chậm thì mới đáng đổi code; “giữ mọi biến một kiểu” không phải yêu cầu semantic của JavaScript.


5. Hidden class (Map): shape object

Object JavaScript linh hoạt, nhưng V8 không coi chúng như hash map generic trong code nóng. Thay vào đó, mỗi object mang hidden class (nội bộ là Map) mô tả layout property hiện tại: tên property, thứ tự, và offset cố định trong bộ nhớ.

function Point(x, y) {
  this.x = x; // hidden class C0 → C1 (adds 'x')
  this.y = y; // hidden class C1 → C2 (adds 'y')
}

const a = new Point(1, 2); // follows C0 → C1 → C2
const b = new Point(3, 4); // same path → same final hidden class C2
// Property access 'a.x' is a direct offset load — fast

Object đi qua cùng transition path từ cùng initial Map có thể chia sẻ Map cuối; tên/thứ tự property là phần quan trọng nhưng prototype, attribute và construction site cũng có thể ảnh hưởng. Khi Map đã biết, optimized code có thể guard Map rồi load field ở offset cố định.

Các thao tác có thể tạo transition/representation khác gồm:

  • Thêm property theo thứ tự khác so với object “tương tự” khác.
  • delete obj.prop hoặc đổi property attribute, có thể khiến object dùng representation tổng quát hơn.
  • Thêm property có điều kiện khiến các instance đi qua transition path khác nhau.

Đừng đổi delete thành gán undefined chỉ để “giữ shape”: hai thao tác có semantics khác nhau (in, Object.keys, serialization). Nếu một hot path thực sự bị shape churn, thiết kế record nhất quán hoặc dùng Map cho key động rồi đo lại.


6. Inline cache: monomorphic, polymorphic, megamorphic

Tại site truy cập property/call, inline cache (IC) và feedback ghi Map/target đã thấy để runtime/compiler chọn handler nhanh hoặc inline operation. Mô hình trạng thái hữu ích là:

IC stateNghĩa
UninitializedChưa có feedback
MonomorphicĐã thấy một Map/target
PolymorphicĐã thấy một tập nhỏ Map/target
MegamorphicNhiều shape; dùng lookup/handler tổng quát hơn

Vòng lặp xử lý record từ cùng construction path có thể giữ site monomorphic để compiler specialize. Threshold chuyển state và cách handler hoạt động là nội bộ, không nên hard-code kiểu “quá 4 shape là chậm”. Tài liệu Maps của V8 cũng cảnh báo cấu trúc này có thể thay đổi.

Quy tắc thực tế: nếu dữ liệu vốn là record đồng nhất, normalize ở boundary giúp cả code lẫn engine. Nếu dữ liệu thật sự là dictionary, dùng Map/object động đúng semantics; đừng thêm field giả vào mọi object nếu profile không chứng minh lợi ích.


7. Số: Smi, HeapNumber và representation tối ưu

V8 biểu diễn small integer dưới dạng Smi tagged; phạm vi bit phụ thuộc build/kiến trúc (pointer compression phổ biến dùng payload signed 31-bit). Giá trị Number khác có thể ở HeapNumber, nhưng optimizing tier cũng có thể giữ integer/double unboxed trong register mà không tạo heap object cho mỗi phép tính.

Vì vậy “double luôn allocation” và “Smi luôn nhanh” đều quá đơn giản. Type feedback không ổn định có thể cần guard/conversion hoặc deopt, nhưng chỉ đáng quan tâm khi numeric hot loop xuất hiện trong profile.

Đừng làm méo model dữ liệu vì representation này; TypedArray, WebAssembly hay Worker thường là đòn bẩy lớn hơn nếu workload số thật sự nặng.


8. Hệ quả thực tế cho code bạn viết

Chi tiết engine hội tụ thành một thứ tự ưu tiên thực dụng:

  1. Giảm code và công việc trước — code splitting, algorithm/data structure, tránh render thừa.
  2. Profile workload thật — DevTools Performance/Bottom-up, CPU profile, allocation profile; tìm hot function trước.
  3. Normalize record ở boundary khi domain vốn đồng nhất; dùng dictionary/Map khi key thật sự động.
  4. Điều tra shape/type churn chỉ trong hot path đã đo; xác nhận sau thay đổi trên cùng workload.
  5. Không phụ thuộc tier/threshold nội bộ — V8 đổi compiler mà code đúng vẫn phải đúng.

Không có nghĩa viết JS kiểu C hay bỏ pattern động mọi nơi — chỉ thay đổi pattern động khi code thực sự chạy nóng. Profile trước (DevTools Performance → bottom-up theo function JS); tối ưu nơi bằng chứng chỉ ra.

// Normalize untrusted/heterogeneous input once at the boundary.
const normalized = rawItems.map((item) => ({
  value: Number(item.value),
  bonus: Number(item.extra ?? 0),
}));

for (const item of normalized) {
  total += item.value + item.bonus;
}

Tóm tắt

  • V8 hiện tier qua Ignition → Sparkplug → Maglev → TurboFan/Turboshaft; workload không nhất thiết đi qua mọi tier.
  • Optimizing compiler dùng feedback và guard; guard fail có thể deopt về tier thấp hơn rồi thu thập feedback mới.
  • Map (hidden class) mô tả shape; object đi cùng transition path có thể cho compiler load field theo offset.
  • IC có thể monomorphic/polymorphic/megamorphic, nhưng threshold và cost là implementation detail.
  • Smi/HeapNumber chỉ là representation; optimized code có thể unbox. Giảm work và profile trước mọi micro-optimization.

Phần tiếp theo: Event loop, task và microtask — cách lập lịch JavaScript gắn với render và vì sao long task đóng băng trang.