Inside V8: How JavaScript Code Becomes Executable
When you write const x = 1 + 2, what actually happens inside the browser? The answer involves a sophisticated pipeline spanning lexical analysis, abstract syntax tree construction, bytecode generation, and multiple tiers of just-in-time compilation. Understanding V8's execution model transforms how you write JavaScript—revealing why certain patterns perform orders of magnitude better than others.
The Scale of JavaScript Execution
Modern web applications push JavaScript engines to their limits:
| Dimension | Scale |
|---|---|
| Script Parse Time | 50-500ms for large bundles |
| Bytecode Generation | 1-10MB of bytecode per page |
| Function Compilations | 10,000-100,000 per session |
| JIT Recompilations | 100-1,000 hot function optimizations |
| Memory for Code | 50-200MB for compiled code |
| Execution Contexts | 1,000-10,000 active contexts |
| Inline Cache Entries | 100,000+ property access caches |
| Deoptimizations | 10-100 per complex application |
GIF via GIPHY
V8 must balance startup speed against peak performance while maintaining memory efficiency—a three-way tradeoff that shapes every architectural decision.
V8 Execution Pipeline Architecture
┌─────────────────────────────────────────────────────────────────────────────┐
│ V8 EXECUTION PIPELINE │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Source │───▶│ Scanner │───▶│ Parser │───▶│ AST │ │
│ │ Code │ │ (Lexer) │ │ │ │ │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ └──────┬──────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ IGNITION (Interpreter) │ │
│ │ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │ │
│ │ │ Bytecode │───▶│ Bytecode │───▶│ Bytecode │ │ │
│ │ │ Generator │ │ Array │ │ Execution │ │ │
│ │ └───────────────┘ └───────────────┘ └───────┬───────┘ │ │
│ └─────────────────────────────────────────────────────┼───────────────┘ │
│ │ │
│ Profiling Data │ │
│ ┌──────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ TURBOFAN (Optimizing Compiler) │ │
│ │ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │ │
│ │ │ Graph │───▶│ Optimization │───▶│ Machine │ │ │
│ │ │ Building │ │ Phases │ │ Code │ │ │
│ │ └───────────────┘ └───────────────┘ └───────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ DEOPTIMIZATION │ │
│ │ Optimized Code ──────▶ Bailout ──────▶ Back to Interpreter │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
GIF via GIPHY
Phase 1: Lexical Analysis (Scanning)
The scanner transforms raw source text into tokens—the atomic units of JavaScript syntax.
GIF via GIPHY
// V8 Scanner Token Types (conceptual representation)
enum TokenType {
// Literals
NUMBER = 'NUMBER',
STRING = 'STRING',
TEMPLATE_LITERAL = 'TEMPLATE_LITERAL',
REGEXP = 'REGEXP',
BIGINT = 'BIGINT',
// Identifiers and Keywords
IDENTIFIER = 'IDENTIFIER',
KEYWORD = 'KEYWORD',
FUTURE_RESERVED = 'FUTURE_RESERVED',
// Punctuators
LBRACE = '{',
RBRACE = '}',
LPAREN = '(',
RPAREN = ')',
LBRACK = '[',
RBRACK = ']',
SEMICOLON = ';',
COMMA = ',',
// Operators
ADD = '+',
SUB = '-',
MUL = '*',
DIV = '/',
MOD = '%',
EXP = '**',
// Assignment
ASSIGN = '=',
ADD_ASSIGN = '+=',
// Comparison
EQ = '==',
STRICT_EQ = '===',
LT = '<',
GT = '>',
// Logical
AND = '&&',
OR = '||',
NULLISH = '??',
// Special
EOF = 'EOF',
ILLEGAL = 'ILLEGAL',
}
interface Token {
type: TokenType;
value: string | number | bigint;
location: SourceLocation;
// V8-specific metadata
precedence?: number;
isKeyword?: boolean;
}
interface SourceLocation {
start: number;
end: number;
line: number;
column: number;
}
class Scanner {
private source: string;
private position: number = 0;
private line: number = 1;
private column: number = 0;
private tokenCache: Token[] = [];
constructor(source: string) {
this.source = source;
}
// V8 uses a one-token lookahead with lazy scanning
scan(): Token {
this.skipWhitespaceAndComments();
if (this.position >= this.source.length) {
return this.makeToken(TokenType.EOF, '');
}
const char = this.source[this.position];
const start = this.position;
// Numeric literals (including hex, binary, octal)
if (this.isDigit(char) || (char === '.' && this.isDigit(this.peek(1)))) {
return this.scanNumericLiteral();
}
// String literals
if (char === '"' || char === "'") {
return this.scanStringLiteral(char);
}
// Template literals
if (char === '`') {
return this.scanTemplateLiteral();
}
// Identifiers and keywords
if (this.isIdentifierStart(char)) {
return this.scanIdentifierOrKeyword();
}
// Operators and punctuators
return this.scanOperatorOrPunctuator();
}
private scanNumericLiteral(): Token {
const start = this.position;
let value: number | bigint;
// Check for special prefixes
if (this.source[this.position] === '0') {
const next = this.source[this.position + 1]?.toLowerCase();
if (next === 'x') {
// Hexadecimal: 0xFF
return this.scanHexLiteral();
} else if (next === 'b') {
// Binary: 0b1010
return this.scanBinaryLiteral();
} else if (next === 'o') {
// Octal: 0o777
return this.scanOctalLiteral();
}
}
// Decimal literal
while (this.isDigit(this.current()) || this.current() === '_') {
if (this.current() !== '_') {
this.advance();
} else {
// Numeric separators (ES2021)
this.advance();
}
}
// Decimal point
if (this.current() === '.' && this.isDigit(this.peek(1))) {
this.advance(); // consume '.'
while (this.isDigit(this.current())) {
this.advance();
}
}
// Exponent
if (this.current() === 'e' || this.current() === 'E') {
this.advance();
if (this.current() === '+' || this.current() === '-') {
this.advance();
}
while (this.isDigit(this.current())) {
this.advance();
}
}
// BigInt suffix
if (this.current() === 'n') {
this.advance();
const raw = this.source.slice(start, this.position - 1).replace(/_/g, '');
return this.makeToken(TokenType.BIGINT, BigInt(raw));
}
const raw = this.source.slice(start, this.position).replace(/_/g, '');
return this.makeToken(TokenType.NUMBER, parseFloat(raw));
}
private scanIdentifierOrKeyword(): Token {
const start = this.position;
while (this.isIdentifierPart(this.current())) {
this.advance();
}
const value = this.source.slice(start, this.position);
// Check if it's a keyword
if (KEYWORDS.has(value)) {
return this.makeToken(TokenType.KEYWORD, value);
}
// Check for contextual keywords (async, await, yield, etc.)
// These are valid identifiers in some contexts
return this.makeToken(TokenType.IDENTIFIER, value);
}
// Unicode-aware identifier checking (ES6+)
private isIdentifierStart(char: string): boolean {
if (!char) return false;
const code = char.charCodeAt(0);
// Fast path: ASCII
if (code < 128) {
return (code >= 65 && code <= 90) || // A-Z
(code >= 97 && code <= 122) || // a-z
code === 36 || // $
code === 95; // _
}
// Slow path: Unicode ID_Start
return /[\p{ID_Start}]/u.test(char);
}
private isIdentifierPart(char: string): boolean {
if (!char) return false;
const code = char.charCodeAt(0);
if (code < 128) {
return (code >= 65 && code <= 90) ||
(code >= 97 && code <= 122) ||
(code >= 48 && code <= 57) ||
code === 36 ||
code === 95;
}
return /[\p{ID_Continue}]/u.test(char);
}
private skipWhitespaceAndComments(): void {
while (this.position < this.source.length) {
const char = this.current();
// Whitespace
if (char === ' ' || char === '\t' || char === '\r') {
this.advance();
continue;
}
// Newline
if (char === '\n') {
this.line++;
this.column = 0;
this.advance();
continue;
}
// Single-line comment
if (char === '/' && this.peek(1) === '/') {
while (this.current() && this.current() !== '\n') {
this.advance();
}
continue;
}
// Multi-line comment
if (char === '/' && this.peek(1) === '*') {
this.advance(); // /
this.advance(); // *
while (this.position < this.source.length) {
if (this.current() === '*' && this.peek(1) === '/') {
this.advance();
this.advance();
break;
}
if (this.current() === '\n') {
this.line++;
this.column = 0;
}
this.advance();
}
continue;
}
break;
}
}
private current(): string {
return this.source[this.position];
}
private peek(offset: number): string {
return this.source[this.position + offset];
}
private advance(): void {
this.position++;
this.column++;
}
private isDigit(char: string): boolean {
return char >= '0' && char <= '9';
}
private makeToken(type: TokenType, value: string | number | bigint): Token {
return {
type,
value,
location: {
start: this.position,
end: this.position,
line: this.line,
column: this.column,
},
};
}
}
const KEYWORDS = new Set([
'break', 'case', 'catch', 'continue', 'debugger', 'default', 'delete',
'do', 'else', 'export', 'extends', 'finally', 'for', 'function', 'if',
'import', 'in', 'instanceof', 'new', 'return', 'super', 'switch', 'this',
'throw', 'try', 'typeof', 'var', 'void', 'while', 'with', 'yield',
'const', 'let', 'static', 'class', 'async', 'await',
]);
Phase 2: Parsing and AST Construction
V8 uses a hand-written recursive descent parser optimized for JavaScript's grammar peculiarities.
GIF via GIPHY
// AST Node Types (subset of V8's actual node types)
type ASTNode =
| Program
| FunctionDeclaration
| VariableDeclaration
| ExpressionStatement
| BinaryExpression
| CallExpression
| MemberExpression
| Identifier
| Literal
| ArrayExpression
| ObjectExpression;
interface Program {
type: 'Program';
body: Statement[];
sourceType: 'script' | 'module';
}
interface FunctionDeclaration {
type: 'FunctionDeclaration';
id: Identifier;
params: Pattern[];
body: BlockStatement;
async: boolean;
generator: boolean;
// V8-specific
kind: FunctionKind;
expectedArgumentCount: number;
hasRestParameter: boolean;
}
enum FunctionKind {
NormalFunction,
ArrowFunction,
GeneratorFunction,
AsyncFunction,
AsyncGeneratorFunction,
Method,
ClassStaticMethod,
GetterFunction,
SetterFunction,
ClassConstructor,
}
interface BinaryExpression {
type: 'BinaryExpression';
operator: string;
left: Expression;
right: Expression;
// V8 tracks operator precedence for optimization
precedence: number;
}
class Parser {
private scanner: Scanner;
private currentToken: Token;
private lookahead: Token | null = null;
// V8's parser maintains scope information during parsing
private scopeStack: Scope[] = [];
private functionState: FunctionState | null = null;
constructor(source: string) {
this.scanner = new Scanner(source);
this.currentToken = this.scanner.scan();
}
parse(): Program {
const body: Statement[] = [];
// Enter global scope
this.enterScope(ScopeType.SCRIPT);
while (!this.isAtEnd()) {
const statement = this.parseStatement();
if (statement) {
body.push(statement);
}
}
this.exitScope();
return {
type: 'Program',
body,
sourceType: 'script',
};
}
private parseStatement(): Statement {
switch (this.currentToken.type) {
case TokenType.KEYWORD:
return this.parseKeywordStatement();
case TokenType.LBRACE:
return this.parseBlockStatement();
default:
return this.parseExpressionStatement();
}
}
private parseKeywordStatement(): Statement {
const keyword = this.currentToken.value as string;
switch (keyword) {
case 'function':
return this.parseFunctionDeclaration();
case 'const':
case 'let':
case 'var':
return this.parseVariableDeclaration();
case 'if':
return this.parseIfStatement();
case 'for':
return this.parseForStatement();
case 'while':
return this.parseWhileStatement();
case 'return':
return this.parseReturnStatement();
case 'class':
return this.parseClassDeclaration();
case 'async':
return this.parseAsyncFunctionOrExpression();
default:
return this.parseExpressionStatement();
}
}
private parseFunctionDeclaration(): FunctionDeclaration {
this.expect(TokenType.KEYWORD, 'function');
const isGenerator = this.match(TokenType.MUL);
const id = this.parseIdentifier();
// Enter function scope
this.enterScope(ScopeType.FUNCTION);
this.functionState = new FunctionState(
isGenerator ? FunctionKind.GeneratorFunction : FunctionKind.NormalFunction
);
this.expect(TokenType.LPAREN);
const params = this.parseParameterList();
this.expect(TokenType.RPAREN);
const body = this.parseBlockStatement();
const functionState = this.functionState;
this.functionState = null;
this.exitScope();
return {
type: 'FunctionDeclaration',
id,
params,
body,
async: false,
generator: isGenerator,
kind: functionState.kind,
expectedArgumentCount: functionState.expectedArgumentCount,
hasRestParameter: functionState.hasRestParameter,
};
}
// V8 implements operator precedence with Pratt parsing
private parseExpression(precedence: number = 0): Expression {
let left = this.parsePrimaryExpression();
while (true) {
const opPrecedence = this.getOperatorPrecedence(this.currentToken);
if (opPrecedence <= precedence) {
break;
}
if (this.isBinaryOperator(this.currentToken)) {
left = this.parseBinaryExpression(left, opPrecedence);
} else if (this.currentToken.type === TokenType.LPAREN) {
left = this.parseCallExpression(left);
} else if (this.currentToken.type === TokenType.LBRACK) {
left = this.parseMemberExpression(left, true);
} else if (this.currentToken.type === '.' as TokenType) {
left = this.parseMemberExpression(left, false);
} else {
break;
}
}
return left;
}
private parseBinaryExpression(left: Expression, precedence: number): BinaryExpression {
const operator = this.currentToken.value as string;
this.advance();
// Right associativity for ** operator
const rightPrecedence = operator === '**' ? precedence - 1 : precedence;
const right = this.parseExpression(rightPrecedence);
return {
type: 'BinaryExpression',
operator,
left,
right,
precedence,
};
}
private getOperatorPrecedence(token: Token): number {
const precedenceMap: Record<string, number> = {
'||': 4,
'??': 4,
'&&': 5,
'|': 6,
'^': 7,
'&': 8,
'==': 9,
'!=': 9,
'===': 9,
'!==': 9,
'<': 10,
'>': 10,
'<=': 10,
'>=': 10,
'instanceof': 10,
'in': 10,
'<<': 11,
'>>': 11,
'>>>': 11,
'+': 12,
'-': 12,
'*': 13,
'/': 13,
'%': 13,
'**': 14, // Right associative
};
return precedenceMap[token.value as string] ?? 0;
}
// Scope management (V8 pre-parses to detect variable declarations)
private enterScope(type: ScopeType): void {
const parent = this.currentScope();
const scope = new Scope(type, parent);
this.scopeStack.push(scope);
}
private exitScope(): Scope {
return this.scopeStack.pop()!;
}
private currentScope(): Scope | null {
return this.scopeStack[this.scopeStack.length - 1] ?? null;
}
private declareVariable(name: string, kind: 'var' | 'let' | 'const'): void {
const scope = this.currentScope();
if (!scope) return;
if (kind === 'var') {
// var is function-scoped, hoist to function scope
scope.declareVar(name);
} else {
// let/const are block-scoped
if (scope.hasLexicalDeclaration(name)) {
throw new SyntaxError(`Identifier '${name}' has already been declared`);
}
scope.declareLexical(name, kind);
}
}
}
enum ScopeType {
SCRIPT,
MODULE,
FUNCTION,
BLOCK,
CATCH,
WITH,
CLASS,
}
class Scope {
type: ScopeType;
parent: Scope | null;
varDeclarations: Set<string> = new Set();
lexicalDeclarations: Map<string, 'let' | 'const'> = new Map();
constructor(type: ScopeType, parent: Scope | null) {
this.type = type;
this.parent = parent;
}
declareVar(name: string): void {
// Hoist to function scope
if (this.type === ScopeType.FUNCTION || this.type === ScopeType.SCRIPT) {
this.varDeclarations.add(name);
} else if (this.parent) {
this.parent.declareVar(name);
}
}
declareLexical(name: string, kind: 'let' | 'const'): void {
this.lexicalDeclarations.set(name, kind);
}
hasLexicalDeclaration(name: string): boolean {
return this.lexicalDeclarations.has(name);
}
}
class FunctionState {
kind: FunctionKind;
expectedArgumentCount: number = 0;
hasRestParameter: boolean = false;
constructor(kind: FunctionKind) {
this.kind = kind;
}
}
Phase 3: Bytecode Generation (Ignition)
Ignition compiles AST to bytecode—a compact, efficient intermediate representation.
GIF via GIPHY
// V8 Bytecode Instructions (subset)
enum Bytecode {
// Load/Store operations
LdaZero, // Load accumulator with 0
LdaSmi, // Load small integer
LdaConstant, // Load from constant pool
LdaUndefined, // Load undefined
LdaNull, // Load null
LdaTrue, // Load true
LdaFalse, // Load false
LdaGlobal, // Load global variable
LdaNamedProperty, // Load named property
LdaKeyedProperty, // Load keyed property
Star, // Store accumulator to register
Mov, // Move register to register
StaGlobal, // Store to global
StaNamedProperty, // Store named property
StaKeyedProperty, // Store keyed property
// Arithmetic
Add,
Sub,
Mul,
Div,
Mod,
Exp,
BitwiseAnd,
BitwiseOr,
BitwiseXor,
ShiftLeft,
ShiftRight,
ShiftRightLogical,
// Comparison
TestEqual,
TestEqualStrict,
TestLessThan,
TestGreaterThan,
TestLessThanOrEqual,
TestGreaterThanOrEqual,
TestInstanceOf,
TestIn,
// Control Flow
Jump,
JumpIfTrue,
JumpIfFalse,
JumpIfToBooleanTrue,
JumpIfToBooleanFalse,
JumpIfNull,
JumpIfNotNull,
JumpIfUndefined,
JumpIfNotUndefined,
JumpLoop, // Backward jump for loops
// Function calls
CallUndefinedReceiver,
CallProperty,
CallWithSpread,
Construct,
ConstructWithSpread,
// Object/Array creation
CreateArrayLiteral,
CreateObjectLiteral,
CreateClosure,
// Context operations
PushContext,
PopContext,
CreateBlockContext,
CreateFunctionContext,
// Special
Return,
Throw,
Debugger,
// Type feedback
TypeOf,
// For-in/of
ForInPrepare,
ForInNext,
ForInContinue,
}
interface BytecodeInstruction {
opcode: Bytecode;
operands: number[];
offset: number;
}
class BytecodeGenerator {
private bytecode: BytecodeInstruction[] = [];
private constantPool: any[] = [];
private registerAllocator: RegisterAllocator;
private labelTable: Map<string, number> = new Map();
private pendingJumps: Map<string, number[]> = new Map();
constructor() {
this.registerAllocator = new RegisterAllocator();
}
generate(ast: Program): BytecodeArray {
for (const statement of ast.body) {
this.generateStatement(statement);
}
// Implicit return undefined at end of script
this.emit(Bytecode.LdaUndefined);
this.emit(Bytecode.Return);
return new BytecodeArray(
this.bytecode,
this.constantPool,
this.registerAllocator.frameSize
);
}
private generateStatement(statement: Statement): void {
switch (statement.type) {
case 'VariableDeclaration':
this.generateVariableDeclaration(statement);
break;
case 'ExpressionStatement':
this.generateExpression(statement.expression);
break;
case 'FunctionDeclaration':
this.generateFunctionDeclaration(statement);
break;
case 'IfStatement':
this.generateIfStatement(statement);
break;
case 'ForStatement':
this.generateForStatement(statement);
break;
case 'ReturnStatement':
this.generateReturnStatement(statement);
break;
}
}
private generateExpression(expr: Expression): void {
switch (expr.type) {
case 'Literal':
this.generateLiteral(expr);
break;
case 'Identifier':
this.generateIdentifier(expr);
break;
case 'BinaryExpression':
this.generateBinaryExpression(expr);
break;
case 'CallExpression':
this.generateCallExpression(expr);
break;
case 'MemberExpression':
this.generateMemberExpression(expr);
break;
case 'ArrayExpression':
this.generateArrayExpression(expr);
break;
case 'ObjectExpression':
this.generateObjectExpression(expr);
break;
case 'ArrowFunctionExpression':
this.generateArrowFunction(expr);
break;
}
}
private generateLiteral(literal: Literal): void {
const value = literal.value;
if (value === undefined) {
this.emit(Bytecode.LdaUndefined);
} else if (value === null) {
this.emit(Bytecode.LdaNull);
} else if (value === true) {
this.emit(Bytecode.LdaTrue);
} else if (value === false) {
this.emit(Bytecode.LdaFalse);
} else if (typeof value === 'number') {
if (Number.isInteger(value) && value >= -128 && value <= 127) {
// Small integer optimization
if (value === 0) {
this.emit(Bytecode.LdaZero);
} else {
this.emit(Bytecode.LdaSmi, value);
}
} else {
const index = this.addConstant(value);
this.emit(Bytecode.LdaConstant, index);
}
} else if (typeof value === 'string') {
const index = this.addConstant(value);
this.emit(Bytecode.LdaConstant, index);
}
}
private generateBinaryExpression(expr: BinaryExpression): void {
// Generate left operand into accumulator
this.generateExpression(expr.left);
// Store in temporary register
const tempReg = this.registerAllocator.allocate();
this.emit(Bytecode.Star, tempReg);
// Generate right operand into accumulator
this.generateExpression(expr.right);
// Perform operation: result = temp op accumulator
const opcodeMap: Record<string, Bytecode> = {
'+': Bytecode.Add,
'-': Bytecode.Sub,
'*': Bytecode.Mul,
'/': Bytecode.Div,
'%': Bytecode.Mod,
'**': Bytecode.Exp,
'&': Bytecode.BitwiseAnd,
'|': Bytecode.BitwiseOr,
'^': Bytecode.BitwiseXor,
'<<': Bytecode.ShiftLeft,
'>>': Bytecode.ShiftRight,
'>>>': Bytecode.ShiftRightLogical,
'==': Bytecode.TestEqual,
'===': Bytecode.TestEqualStrict,
'<': Bytecode.TestLessThan,
'>': Bytecode.TestGreaterThan,
'<=': Bytecode.TestLessThanOrEqual,
'>=': Bytecode.TestGreaterThanOrEqual,
};
const opcode = opcodeMap[expr.operator];
if (opcode) {
this.emit(opcode, tempReg);
}
this.registerAllocator.free(tempReg);
}
private generateIfStatement(stmt: IfStatement): void {
// Generate condition
this.generateExpression(stmt.test);
const elseLabel = this.createLabel('else');
const endLabel = this.createLabel('endif');
// Jump to else if condition is falsy
this.emitJump(Bytecode.JumpIfToBooleanFalse, elseLabel);
// Generate consequent
this.generateStatement(stmt.consequent);
if (stmt.alternate) {
this.emitJump(Bytecode.Jump, endLabel);
}
// Else branch
this.bindLabel(elseLabel);
if (stmt.alternate) {
this.generateStatement(stmt.alternate);
}
this.bindLabel(endLabel);
}
private generateForStatement(stmt: ForStatement): void {
// Loop structure:
// init
// loop:
// test -> exit if false
// body
// update
// jump loop
// exit:
const loopLabel = this.createLabel('loop');
const exitLabel = this.createLabel('exit');
// Init
if (stmt.init) {
if (stmt.init.type === 'VariableDeclaration') {
this.generateVariableDeclaration(stmt.init);
} else {
this.generateExpression(stmt.init);
}
}
// Loop start
this.bindLabel(loopLabel);
// Test
if (stmt.test) {
this.generateExpression(stmt.test);
this.emitJump(Bytecode.JumpIfToBooleanFalse, exitLabel);
}
// Body
this.generateStatement(stmt.body);
// Update
if (stmt.update) {
this.generateExpression(stmt.update);
}
// Back edge (V8 uses JumpLoop for backward jumps to enable OSR)
this.emitJump(Bytecode.JumpLoop, loopLabel);
// Exit
this.bindLabel(exitLabel);
}
private generateCallExpression(expr: CallExpression): void {
// Allocate registers for arguments
const argRegisters: number[] = [];
for (const arg of expr.arguments) {
this.generateExpression(arg);
const reg = this.registerAllocator.allocate();
this.emit(Bytecode.Star, reg);
argRegisters.push(reg);
}
// Generate callee
if (expr.callee.type === 'MemberExpression') {
// Method call: obj.method(args)
this.generateExpression(expr.callee.object);
const receiverReg = this.registerAllocator.allocate();
this.emit(Bytecode.Star, receiverReg);
const nameIndex = this.addConstant(
(expr.callee.property as Identifier).name
);
this.emit(
Bytecode.CallProperty,
receiverReg,
nameIndex,
argRegisters[0] ?? receiverReg,
expr.arguments.length
);
this.registerAllocator.free(receiverReg);
} else {
// Direct call
this.generateExpression(expr.callee);
const calleeReg = this.registerAllocator.allocate();
this.emit(Bytecode.Star, calleeReg);
this.emit(
Bytecode.CallUndefinedReceiver,
calleeReg,
argRegisters[0] ?? calleeReg,
expr.arguments.length
);
this.registerAllocator.free(calleeReg);
}
// Free argument registers
for (const reg of argRegisters) {
this.registerAllocator.free(reg);
}
}
private emit(opcode: Bytecode, ...operands: number[]): void {
this.bytecode.push({
opcode,
operands,
offset: this.bytecode.length,
});
}
private emitJump(opcode: Bytecode, label: string): void {
const jumpOffset = this.bytecode.length;
this.emit(opcode, 0); // Placeholder offset
if (!this.pendingJumps.has(label)) {
this.pendingJumps.set(label, []);
}
this.pendingJumps.get(label)!.push(jumpOffset);
}
private createLabel(prefix: string): string {
return `${prefix}_${this.labelTable.size}`;
}
private bindLabel(label: string): void {
const targetOffset = this.bytecode.length;
this.labelTable.set(label, targetOffset);
// Patch pending jumps to this label
const jumps = this.pendingJumps.get(label);
if (jumps) {
for (const jumpOffset of jumps) {
this.bytecode[jumpOffset].operands[0] = targetOffset - jumpOffset;
}
this.pendingJumps.delete(label);
}
}
private addConstant(value: any): number {
const existing = this.constantPool.indexOf(value);
if (existing !== -1) {
return existing;
}
this.constantPool.push(value);
return this.constantPool.length - 1;
}
}
class RegisterAllocator {
private registers: boolean[] = [];
frameSize: number = 0;
allocate(): number {
// Find first free register
for (let i = 0; i < this.registers.length; i++) {
if (!this.registers[i]) {
this.registers[i] = true;
return i;
}
}
// Allocate new register
const index = this.registers.length;
this.registers.push(true);
this.frameSize = Math.max(this.frameSize, this.registers.length);
return index;
}
free(index: number): void {
this.registers[index] = false;
}
}
class BytecodeArray {
instructions: BytecodeInstruction[];
constantPool: any[];
frameSize: number;
constructor(
instructions: BytecodeInstruction[],
constantPool: any[],
frameSize: number
) {
this.instructions = instructions;
this.constantPool = constantPool;
this.frameSize = frameSize;
}
disassemble(): string {
const lines: string[] = [];
for (const instr of this.instructions) {
const operandStr = instr.operands
.map(op => this.formatOperand(instr.opcode, op))
.join(', ');
lines.push(
`${instr.offset.toString().padStart(4, '0')}: ${Bytecode[instr.opcode]}${
operandStr ? ' ' + operandStr : ''
}`
);
}
return lines.join('\n');
}
private formatOperand(opcode: Bytecode, operand: number): string {
if (opcode === Bytecode.LdaConstant || opcode === Bytecode.LdaNamedProperty) {
return `[${operand}] (${JSON.stringify(this.constantPool[operand])})`;
}
if (opcode === Bytecode.Star || opcode === Bytecode.Add) {
return `r${operand}`;
}
return operand.toString();
}
}
Phase 4: Bytecode Interpretation
Ignition executes bytecode using a register-based interpreter.
GIF via GIPHY
// Simplified Ignition interpreter model
class BytecodeInterpreter {
private bytecode: BytecodeArray;
private pc: number = 0; // Program counter
private accumulator: any; // Special register
private registers: any[];
private context: ExecutionContext;
constructor(bytecode: BytecodeArray, context: ExecutionContext) {
this.bytecode = bytecode;
this.registers = new Array(bytecode.frameSize).fill(undefined);
this.context = context;
}
execute(): any {
while (this.pc < this.bytecode.instructions.length) {
const instruction = this.bytecode.instructions[this.pc];
const result = this.dispatch(instruction);
if (result?.type === 'return') {
return result.value;
}
this.pc++;
}
return undefined;
}
private dispatch(instruction: BytecodeInstruction): any {
const { opcode, operands } = instruction;
// V8 uses a dispatch table (computed goto) for efficient dispatch
// This switch simulates that behavior
switch (opcode) {
case Bytecode.LdaZero:
this.accumulator = 0;
break;
case Bytecode.LdaSmi:
this.accumulator = operands[0];
break;
case Bytecode.LdaConstant:
this.accumulator = this.bytecode.constantPool[operands[0]];
break;
case Bytecode.LdaUndefined:
this.accumulator = undefined;
break;
case Bytecode.LdaNull:
this.accumulator = null;
break;
case Bytecode.LdaTrue:
this.accumulator = true;
break;
case Bytecode.LdaFalse:
this.accumulator = false;
break;
case Bytecode.Star:
this.registers[operands[0]] = this.accumulator;
break;
case Bytecode.Mov:
this.registers[operands[1]] = this.registers[operands[0]];
break;
case Bytecode.LdaGlobal:
const globalName = this.bytecode.constantPool[operands[0]];
this.accumulator = this.context.global[globalName];
break;
case Bytecode.StaGlobal:
const storeName = this.bytecode.constantPool[operands[0]];
this.context.global[storeName] = this.accumulator;
break;
case Bytecode.Add:
this.accumulator = this.registers[operands[0]] + this.accumulator;
// Record type feedback for optimization
this.recordTypeFeedback(instruction,
typeof this.registers[operands[0]],
typeof this.accumulator
);
break;
case Bytecode.Sub:
this.accumulator = this.registers[operands[0]] - this.accumulator;
break;
case Bytecode.Mul:
this.accumulator = this.registers[operands[0]] * this.accumulator;
break;
case Bytecode.Div:
this.accumulator = this.registers[operands[0]] / this.accumulator;
break;
case Bytecode.Mod:
this.accumulator = this.registers[operands[0]] % this.accumulator;
break;
case Bytecode.TestEqual:
this.accumulator = this.registers[operands[0]] == this.accumulator;
break;
case Bytecode.TestEqualStrict:
this.accumulator = this.registers[operands[0]] === this.accumulator;
break;
case Bytecode.TestLessThan:
this.accumulator = this.registers[operands[0]] < this.accumulator;
break;
case Bytecode.TestGreaterThan:
this.accumulator = this.registers[operands[0]] > this.accumulator;
break;
case Bytecode.Jump:
this.pc += operands[0] - 1; // -1 because pc++ happens after
break;
case Bytecode.JumpIfTrue:
if (this.accumulator === true) {
this.pc += operands[0] - 1;
}
break;
case Bytecode.JumpIfFalse:
if (this.accumulator === false) {
this.pc += operands[0] - 1;
}
break;
case Bytecode.JumpIfToBooleanTrue:
if (this.toBoolean(this.accumulator)) {
this.pc += operands[0] - 1;
}
break;
case Bytecode.JumpIfToBooleanFalse:
if (!this.toBoolean(this.accumulator)) {
this.pc += operands[0] - 1;
}
break;
case Bytecode.JumpLoop:
// Backward jump - check for interrupt/OSR opportunity
this.checkInterrupt();
this.pc += operands[0] - 1;
break;
case Bytecode.CallUndefinedReceiver:
const callee = this.registers[operands[0]];
const firstArg = operands[1];
const argCount = operands[2];
const args = this.registers.slice(firstArg, firstArg + argCount);
this.accumulator = callee.apply(undefined, args);
break;
case Bytecode.Return:
return { type: 'return', value: this.accumulator };
case Bytecode.CreateArrayLiteral:
const arrayElements = this.bytecode.constantPool[operands[0]];
this.accumulator = [...arrayElements];
break;
case Bytecode.CreateObjectLiteral:
const objectTemplate = this.bytecode.constantPool[operands[0]];
this.accumulator = Object.assign({}, objectTemplate);
break;
}
return null;
}
private toBoolean(value: any): boolean {
// JavaScript's ToBoolean abstract operation
if (value === undefined || value === null) return false;
if (typeof value === 'boolean') return value;
if (typeof value === 'number') return value !== 0 && !Number.isNaN(value);
if (typeof value === 'string') return value.length > 0;
if (typeof value === 'bigint') return value !== 0n;
if (typeof value === 'symbol') return true;
return true; // Objects are always truthy
}
// Type feedback collection for TurboFan
private typeFeedbackVector: Map<number, TypeFeedback> = new Map();
private recordTypeFeedback(
instruction: BytecodeInstruction,
leftType: string,
rightType: string
): void {
const existing = this.typeFeedbackVector.get(instruction.offset);
if (!existing) {
this.typeFeedbackVector.set(instruction.offset, {
leftTypes: new Set([leftType]),
rightTypes: new Set([rightType]),
executionCount: 1,
});
} else {
existing.leftTypes.add(leftType);
existing.rightTypes.add(rightType);
existing.executionCount++;
// Mark as polymorphic if too many types seen
if (existing.leftTypes.size > 4 || existing.rightTypes.size > 4) {
existing.megamorphic = true;
}
}
}
private checkInterrupt(): void {
// In real V8, this checks for:
// - Stack overflow
// - Debugger breakpoints
// - On-Stack Replacement (OSR) opportunities
// - GC safepoints
const feedback = this.getHotLoopFeedback();
if (feedback && feedback.executionCount > 1000) {
// Trigger OSR compilation to optimized code
this.triggerOSR();
}
}
private getHotLoopFeedback(): TypeFeedback | null {
// Find feedback for current loop
return null;
}
private triggerOSR(): void {
// On-Stack Replacement: compile hot loop and replace
// interpreter frame with optimized code frame
console.log('OSR triggered - would compile hot loop');
}
}
interface TypeFeedback {
leftTypes: Set<string>;
rightTypes: Set<string>;
executionCount: number;
megamorphic?: boolean;
}
interface ExecutionContext {
global: Record<string, any>;
// Scope chain, this binding, etc.
}
Phase 5: TurboFan Optimization
TurboFan compiles hot bytecode to optimized machine code using collected type feedback.
GIF via GIPHY
// TurboFan compilation pipeline (conceptual)
class TurboFanCompiler {
compile(
bytecode: BytecodeArray,
typeFeedback: Map<number, TypeFeedback>
): OptimizedCode {
// Phase 1: Build graph from bytecode
const graph = this.buildGraph(bytecode, typeFeedback);
// Phase 2: Run optimization phases
this.runOptimizations(graph);
// Phase 3: Instruction selection
const machineCode = this.selectInstructions(graph);
// Phase 4: Register allocation
this.allocateRegisters(machineCode);
// Phase 5: Code generation
return this.generateCode(machineCode);
}
private buildGraph(
bytecode: BytecodeArray,
typeFeedback: Map<number, TypeFeedback>
): IRGraph {
const graph = new IRGraph();
// Convert bytecode to SSA form
for (const instruction of bytecode.instructions) {
const feedback = typeFeedback.get(instruction.offset);
const node = this.bytecodeToNode(instruction, feedback);
graph.addNode(node);
}
return graph;
}
private bytecodeToNode(
instruction: BytecodeInstruction,
feedback: TypeFeedback | undefined
): IRNode {
switch (instruction.opcode) {
case Bytecode.Add:
// Specialize based on type feedback
if (feedback && !feedback.megamorphic) {
if (this.isMonomorphic(feedback, 'number')) {
return new SpeculativeNumberAdd(
instruction,
this.createDeoptPoint(instruction)
);
}
if (this.isMonomorphic(feedback, 'string')) {
return new StringConcat(instruction);
}
}
// Fallback to generic add
return new GenericAdd(instruction);
case Bytecode.LdaNamedProperty:
// Use inline cache information
if (feedback && !feedback.megamorphic) {
return new LoadNamedPropertyIC(
instruction,
feedback,
this.createDeoptPoint(instruction)
);
}
return new GenericLoadProperty(instruction);
default:
return new GenericNode(instruction);
}
}
private runOptimizations(graph: IRGraph): void {
// TurboFan runs ~20 optimization phases
const phases = [
new TypeNarrowingPhase(),
new InliningPhase(),
new LoadEliminationPhase(),
new EscapeAnalysisPhase(),
new SimplifiedLoweringPhase(),
new GenericLoweringPhase(),
new DeadCodeEliminationPhase(),
new LoopOptimizationPhase(),
new BranchEliminationPhase(),
new MemoryOptimizationPhase(),
];
for (const phase of phases) {
phase.run(graph);
}
}
private isMonomorphic(feedback: TypeFeedback, type: string): boolean {
return feedback.leftTypes.size === 1 &&
feedback.leftTypes.has(type) &&
feedback.rightTypes.size === 1 &&
feedback.rightTypes.has(type);
}
private createDeoptPoint(instruction: BytecodeInstruction): DeoptimizationPoint {
return {
bytecodeOffset: instruction.offset,
reason: 'type check failed',
};
}
}
// Sea of Nodes IR (simplified)
class IRGraph {
private nodes: IRNode[] = [];
private startNode: IRNode;
private endNode: IRNode;
addNode(node: IRNode): void {
this.nodes.push(node);
}
}
interface IRNode {
id: number;
inputs: IRNode[];
uses: IRNode[];
opcode: string;
}
class SpeculativeNumberAdd implements IRNode {
id: number;
inputs: IRNode[] = [];
uses: IRNode[] = [];
opcode = 'SpeculativeNumberAdd';
deoptPoint: DeoptimizationPoint;
constructor(
instruction: BytecodeInstruction,
deoptPoint: DeoptimizationPoint
) {
this.id = instruction.offset;
this.deoptPoint = deoptPoint;
}
// This compiles to:
// 1. Type check: is left a number?
// 2. Type check: is right a number?
// 3. If checks fail: jump to deoptimization
// 4. If checks pass: emit efficient ADD instruction
generateCode(): string[] {
return [
'CheckSmi left', // Check if Smi (small integer)
'CheckSmi right', // Check if Smi
'DeoptIf NotSmi', // Deopt if type assumption violated
'SmiAdd left, right', // Fast path: Smi addition
];
}
}
interface DeoptimizationPoint {
bytecodeOffset: number;
reason: string;
}
// Optimization phases
class InliningPhase {
run(graph: IRGraph): void {
// Inline small, frequently called functions
// V8 uses a budget-based system:
// - Small functions (< 200 bytecodes) always inlined
// - Medium functions inlined if hot enough
// - Large functions rarely inlined
// Inlining enables further optimizations:
// - Constant propagation across call boundaries
// - Escape analysis can stack-allocate objects
// - Type information propagates through inlined code
}
}
class EscapeAnalysisPhase {
run(graph: IRGraph): void {
// Determine if objects escape their allocation site
// If not, they can be:
// - Stack allocated (no GC pressure)
// - Scalar replaced (eliminate object entirely)
// Example:
// function Point(x, y) { this.x = x; this.y = y; }
// function distance(p) { return Math.sqrt(p.x*p.x + p.y*p.y); }
// distance(new Point(3, 4))
//
// After escape analysis + scalar replacement:
// - No Point object is allocated
// - x and y become local variables
// - Equivalent to: Math.sqrt(3*3 + 4*4)
}
}
class LoadEliminationPhase {
run(graph: IRGraph): void {
// Eliminate redundant loads
// Example:
// let x = obj.field;
// let y = obj.field; // Can reuse x
// Track "alias sets" to handle:
// - Different objects with same shape
// - Stores that might invalidate cached loads
// - Function calls that might modify state
}
}
interface OptimizedCode {
machineCode: ArrayBuffer;
deoptimizationData: DeoptimizationData;
sourcePositionTable: SourcePositionTable;
}
interface DeoptimizationData {
points: DeoptimizationPoint[];
// Maps machine code addresses to bytecode positions
// Used when we need to "bail out" to interpreter
}
interface SourcePositionTable {
// Maps machine code offsets to source positions
// Used for stack traces and debugging
}
Execution Pipeline Visualization
┌─────────────────────────────────────────────────────────────────────────────┐
│ JAVASCRIPT EXECUTION TIMELINE │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ TIME ──────────────────────────────────────────────────────────────────▶ │
│ │
│ ┌────────┐ ┌────────┐ ┌────────────────────────────────────────────┐ │
│ │ Parse │ │Compile │ │ Interpret │ │
│ │ AST │─▶│Bytecode│─▶│ Ignition executes bytecode │ │
│ └────────┘ └────────┘ │ Collects type feedback │ │
│ └────────────────────┬───────────────────────┘ │
│ │ │
│ │ Hot function detected │
│ │ (>10k executions) │
│ ▼ │
│ ┌────────────────────────────────────────────┐ │
│ │ TurboFan Compile │ │
│ │ Uses type feedback to specialize │ │
│ │ Runs optimization phases │ │
│ │ Generates machine code │ │
│ └────────────────────┬───────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────┐ │
│ │ Execute Optimized Code │ │
│ │ Fast machine code execution │ │
│ │ Guards check type assumptions │ │
│ └────────────────────┬───────────────────────┘ │
│ │ │
│ │ Type assumption violated │
│ ▼ │
│ ┌────────────────────────────────────────────┐ │
│ │ Deoptimization │ │
│ │ Throw away optimized code │ │
│ │ Return to interpreter │ │
│ │ May re-optimize later with new feedback │ │
│ └────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
GIF via GIPHY
Understanding V8 Execution with Examples
// Example 1: Simple function optimization
function add(a, b) {
return a + b;
}
// First ~10,000 calls: Ignition interprets
// Collects type feedback: a=number, b=number
// TurboFan compiles with speculation:
// - Assume a is always Smi (small integer)
// - Assume b is always Smi
// - Generate: SmiAdd instruction (single CPU instruction)
// Call with string:
add("hello", "world");
// Type guard fails → deoptimization
// Returns to interpreter
// New feedback collected: a=string|number, b=string|number
// May recompile as polymorphic (handles both types)
// Example 2: Object shape transitions
const objects = [];
for (let i = 0; i < 1000; i++) {
const obj = {};
obj.x = i; // Creates hidden class: Map1 {x}
obj.y = i * 2; // Transitions to: Map2 {x, y}
objects.push(obj);
}
// All 1000 objects share the same hidden class
// Property access compiles to direct offset load:
// obj.x → load [obj + 16] // Direct memory offset
// But if one object is different:
objects[500].z = 1; // Creates new hidden class
// Now objects[500] has different shape
// Inline caches see multiple shapes → polymorphic
// Performance degrades
// Example 3: Function inlining
function square(x) {
return x * x;
}
function sumOfSquares(arr) {
let sum = 0;
for (let i = 0; i < arr.length; i++) {
sum += square(arr[i]); // Hot call site
}
return sum;
}
// TurboFan inlines square():
// function sumOfSquares_optimized(arr) {
// let sum = 0;
// for (let i = 0; i < arr.length; i++) {
// const x = arr[i];
// sum += x * x; // Inlined
// }
// return sum;
// }
// Further optimizations:
// - Array bounds check hoisting
// - Loop unrolling
// - SIMD instructions for numeric arrays
GIF via GIPHY
Production Incidents
Incident 1: Parse Time Explosion
Symptoms: Page load time increased 3x after adding a new analytics library.
Investigation:
// The analytics library included this pattern:
const lookup = {
"event_001": () => { /* handler */ },
"event_002": () => { /* handler */ },
// ... 5000 more entries
"event_5000": () => { /* handler */ },
};
// V8 has to:
// 1. Parse 5000 arrow functions
// 2. Create AST nodes for each
// 3. Generate bytecode for each
// Even though most are never called
Root Cause: V8's eager parsing created 5000 function objects at load time. Each function required AST construction, bytecode generation, and memory allocation.
Fix:
// Lazy initialization pattern
const eventHandlers = new Map();
const handlerFactories = {
"event_001": () => () => { /* handler */ },
// Outer function is parsed but inner is deferred
};
function getHandler(eventName) {
if (!eventHandlers.has(eventName)) {
const factory = handlerFactories[eventName];
if (factory) {
eventHandlers.set(eventName, factory());
}
}
return eventHandlers.get(eventName);
}
Result: Parse time dropped from 800ms to 150ms.
Incident 2: Megamorphic Property Access
Symptoms: Application became progressively slower over time. CPU profiler showed 40% time in property access.
Investigation:
// State management code
function updateState(state, action) {
switch (action.type) {
case 'SET_USER':
return { ...state, user: action.payload };
case 'SET_SETTINGS':
return { ...state, settings: action.payload };
// ... 50 more cases
}
}
// Each case creates object with different hidden class
// After 4 different shapes, V8 gives up optimizing
// Property access becomes megamorphic (dictionary lookup)
GIF via GIPHY
Root Cause: Each { ...state, newProp } created objects with different property order, leading to different hidden classes. After seeing >4 shapes, inline caches became megamorphic.
Fix:
// Consistent object shape
class AppState {
user = null;
settings = null;
cart = null;
// Declare all properties upfront
update(key, value) {
const newState = new AppState();
Object.assign(newState, this);
newState[key] = value;
return newState;
}
}
// Or use immer for structural sharing
import { produce } from 'immer';
const newState = produce(state, draft => {
draft.user = action.payload;
});
Result: Property access time reduced by 85%.
Incident 3: Deoptimization Loop
Symptoms: Function showed 10x variance in execution time. Trace showed repeated optimization/deoptimization cycles.
Investigation:
function processItems(items) {
let total = 0;
for (const item of items) {
total += item.value; // Usually number, occasionally string
}
return total;
}
// V8 trace showed:
// [marking for optimization] processItems
// [optimizing] processItems
// [deoptimizing] processItems - wrong types
// [marking for optimization] processItems
// ... (loop continues)
Root Cause: 99% of items had numeric value, but occasional strings caused deoptimization. V8 would reoptimize based on recent numeric data, then deoptimize again on string.
Fix:
function processItems(items) {
let total = 0;
for (const item of items) {
// Explicit type coercion - stable type for V8
const value = Number(item.value);
if (!Number.isNaN(value)) {
total += value;
}
}
return total;
}
// Or handle types explicitly:
function processItems(items) {
let total = 0;
for (const item of items) {
if (typeof item.value === 'number') {
total += item.value;
} else {
total += parseFloat(item.value) || 0;
}
}
return total;
}
Result: Execution time variance reduced from 10x to <1.2x.
Tradeoffs and Engineering Decisions
| Decision | Alternative | Why V8 Chose This |
|---|---|---|
| Bytecode interpreter (Ignition) | Direct AST interpretation | Lower memory, faster startup, enables OSR |
| Two-tier JIT (Ignition + TurboFan) | Single optimizing compiler | Balances startup speed vs peak performance |
| Type feedback speculation | Static type analysis | JavaScript's dynamic types require runtime info |
| Hidden classes | Hash tables for all objects | Enables fast property access like static languages |
| Inline caching | Always polymorphic dispatch | Monomorphic sites (90%+) get static-like speed |
| Lazy parsing | Eager parse everything | Reduces initial parse time for unused functions |
| Sea of Nodes IR | Traditional CFG | Better optimization opportunities, easier analysis |
| Register-based bytecode | Stack-based bytecode | Fewer instructions, maps better to machine |
| Generational GC | Mark-sweep only | Young objects die quickly, optimize for that |
| OSR (On-Stack Replacement) | Wait for function exit | Hot loops get optimized mid-execution |
GIF via GIPHY
Key Takeaways
- V8 execution is a pipeline: Source → Tokens → AST → Bytecode → Optimized Code
- Type stability matters: Consistent types enable aggressive optimization
- Object shapes must be consistent: Hidden classes enable fast property access
- Hot code gets optimized: Type feedback from interpretation guides optimization
- Deoptimization is expensive: Wrong type assumptions force bailout to interpreter
- Inlining is the gateway optimization: Enables escape analysis, constant propagation
- Parse time is non-trivial: Large codebases pay parse cost upfront
- Megamorphism kills performance: Keep polymorphism under 4 shapes
- OSR enables loop optimization: Hot loops don't wait for function completion
- Speculation requires guards: Every optimization is guarded by type checks
GIF via GIPHY
Understanding V8's execution model transforms JavaScript from a "slow scripting language" into a vehicle for highly optimized computation—but only when you write code that V8 can optimize.
What did you think?