Type feedback & feedback vectors
Learn how JavaScript engines collect runtime type feedback in vectors and use warm-up evidence to specialize hot code safely.
- 01Explain feedback collectionDescribe how an engine records the values actually seen by bytecode sites instead of relying on static JavaScript types.
- 02Read slots and statesConnect bytecode operands such as
[0]with FeedbackVector slots for operators, property access, calls, loops, and literals. - 03Warm up code honestlyPredict why cold runs are slow, why one surprising value widens feedback, and how to benchmark hot code without fooling yourself.
Engines learn from the values your code actually uses
JavaScript has dynamic values, not static runtime type declarations. An optimizing compiler therefore cannot safely assume that a + b always means small-integer addition just because the first call used numbers. It watches the program run, records what each operation site saw, and later speculates from that evidence.
Type feedback is runtime evidence stored by a JavaScript engine for a particular bytecode operation: the kinds of operands, targets, object maps, or allocation patterns that site has actually seen.
This lesson sits between tiered compilation and inline caches. We focus on where the evidence lives and how it changes. Later lessons on speculative optimization and deoptimization explain how optimizing compilers consume this feedback and recover when a guess is wrong.
Imagine a cafe where each station writes down what it sees. If the morning rush at the espresso station is always double shots, the barista prepares that station for espresso. If one customer asks for a smoothie at the same station, the old shortcut is no longer safe. Type feedback works the same way for hot JavaScript operations.
- In real life: The barista watches real customers
- In JavaScript: The interpreter observes actual calls
- In real life: One drink station has a notebook
- In JavaScript: One bytecode site has a feedback slot
- In real life: Usual espresso orders can be prepared early
- In JavaScript: A stable type pattern can get a specialized fast path
- In real life: A surprising smoothie order changes the plan
- In JavaScript: A new type widens the feedback and may deoptimize
Where the analogy stops: A barista can use judgment and memory across the whole cafe. Engine feedback is mechanical, site-local, and must preserve exact JavaScript semantics even when the pattern changes.
Keep the prerequisites nearby: data types, numbers in depth, type coercion, bytecode and interpreters, and measuring performance.
Collecting types at runtime
INTERPRETER EVIDENCEFeedback starts cold. The interpreter runs bytecode and records what each interesting operation observes. In V8, Ignition collects this information while executing bytecode. TurboFan, Maglev, and inline-cache machinery can later use it, but the first job is just to gather honest evidence from real values.
const slot = createSlot("add slot #0"); function createSlot(id) { return { id, state: "None", count: 0 };} function classify(left, right) { if (Number.isInteger(left) && Number.isInteger(right)) return "SignedSmall"; if (typeof left === "number" && typeof right === "number") return "Number"; if (typeof left === "string" && typeof right === "string") return "String"; return "Any";} function merge(previous, observed) { if (previous === "None" || previous === observed) return observed; if ((previous === "SignedSmall" && observed === "Number") || (previous === "Number" && observed === "SignedSmall")) return "Number"; return "Any";} function specialize(state) { return { SignedSmall: "int add fast path", Number: "number add fast path", String: "string concatenate fast path", Any: "generic add path", }[state] ?? "collecting in the interpreter";} function callAdd(slot, left, right) { const observed = classify(left, right); slot.state = merge(slot.state, observed); slot.count += 1; const result = left + right; return { result, summary: slot.state + " -> " + specialize(slot.state), };} function callOptimizedAdd(plan, left, right) { return left + right;} console.log(callAdd(slot, 1, 2).summary);console.log(callAdd(slot, 1.5, 2.25).summary);console.log(callAdd(slot, "1", "2").summary);Line 1 creates one empty slot. Lines 7 through 12 classify a single + call. Lines 14 through 18 model the important rule: feedback gets more general, not more specific. The first log is SignedSmall -> int add fast path. The second log widens to Number. The string call makes this same site generic because JavaScript + can mean concatenation too.
Replay a toy feedback slot as it learns from real calls. The state moves from none, to small integers, to general numbers.
script
callAdd(slot, 1, 2);callAdd(slot, 3, 4);callAdd(slot, 1.5, 2.25);Notice the site-local part. A different + expression elsewhere in the function would have its own slot and could remain string-only or integer-only. Engines optimize sites, not the entire operator globally.
Feedback vectors and slots
V8 SHAPEIn V8, the metadata describing which slots a function needs belongs to the shared function information. The runtime feedback itself lives in a FeedbackVector. A modern V8 JSFunction points through a FeedbackCell, which lets V8 delay allocating the full vector until the function becomes hot enough. V8 calls this lazy feedback allocation.
V8's public function-architecture document describes JSFunction, FeedbackCell, and FeedbackVector. The V8 Lite blog explains why feedback vectors are allocated lazily after enough bytecode executes, saving memory for functions that never become hot.
$ node --print-bytecode --print-bytecode-filter=add add.js[generated bytecode for function: add]Parameter count 3 19 S> ... @ 0 : 0b 04 Ldar a1 28 E> ... @ 2 : 3b 03 00 Add a0, [0] 32 S> ... @ 5 : ae ReturnThe [0] in Add a0, [0] is not an array literal. It is the feedback slot operand for that add bytecode. When the function runs enough, V8 can allocate a feedback vector whose slot 0 records the binary operation hint.
A simplified add lattice:
NoneSignedSmallNumberNumberOrOddballAny
Strings and BigInts form separate branches in real V8 hints: String, StringOrStringWrapper, BigInt64, and BigInt also exist before a site reaches Any.
Feedback for calls, properties, operators, loops, and literals
SLOT KINDSV8's feedback slot kinds include calls, property loads, keyed loads, stores, binary operations, comparisons, literals, for...in, instanceof, typeof, object cloning, string-add-and-internalize, and jump-loop feedback. You do not need to memorize the list, but it explains why feedback is richer than “the type of a variable.”
| Slot kind | What it records | Why it matters |
|---|---|---|
| BinaryOp / CompareOp | Operand hints such as SignedSmall, Number, NumberOrOddball, String, BigInt64, BigInt, or Any. | Arithmetic, comparisons, and the + operator's numeric/string split. |
| LoadProperty / Store / keyed access | Maps or shapes plus handlers for how to load or store the property. | Feeds inline caches; the inline-caches lesson goes deep on IC states. |
| Call | The target function and a call count-like payload in the slot. | Helps decide whether a hot call site is stable enough to inline. |
| ForIn / InstanceOf | Enumeration-cache hints or constructor/prototype feedback. | Speeds repeated loops and membership checks when shapes stay predictable. |
| Literal / allocation site | A boilerplate or allocation site for arrays, objects, and literals. | Lets engines reuse literal structure and track allocation behavior. |
function readName(user) { return user.name;} console.log(readName({ name: "Ada", role: "admin" }));console.log(readName({ name: "Grace", role: "engineer" }));A property load such as user.name records the object maps or shapes it has seen. That feeds the inline-cache states you will study in Inline caches. This lesson names the evidence; the next one names the monomorphic, polymorphic, and megamorphic states in depth.
function apply(fn, value) { return fn(value);} function inc(value) { return value + 1;} let calls = 0;for (const value of [1, 2, 3]) { calls += 1; apply(inc, value);} console.log("target: " + inc.name);console.log("calls: " + calls);A call slot can record a stable target function and call information. Optimizers use that evidence when deciding whether a call is worth inlining. Literal feedback points at allocation sites or boilerplates, so repeated object and array literals do not start from scratch every time.
Why the first runs are slow, and what warm-up means
COLD VS HOTA cold function pays setup costs: parsing or compiling bytecode, running in the interpreter, allocating feedback structures lazily, filling slots, and waiting until tiering heuristics decide the function is hot enough. The first few runs are often the least representative runs in a benchmark.
function add(a, b) { return a + b;} for (let i = 0; i < 10000; i += 1) { add(i, i + 1);} console.log("warmed with 10000 number calls");console.log(add(1, 2));The loop does not make add faster by magic. It gives the engine a chance to collect number feedback and, if the function stays hot, compile a plan for the values it actually sees. Warm-up only helps if the warm-up values match the measured workload.
Replay a toy optimizer: feedback creates a guarded fast path, and a surprising type falls back without changing JavaScript's result.
script
callAdd(slot, 1, 2);const plan = specialize(slot.state);callOptimizedAdd(plan, 3, 4);callOptimizedAdd(plan, "3", "4");If a benchmark warms with numbers and measures strings, it measured a type transition. Sometimes that is exactly the scenario you care about. Most of the time it is accidental noise. Use the benchmarking habits from Measuring performance: warm up, keep results used, run many samples, and report variance or medians.
Real V8 evidence from Node 22.23.1
V8 12.4The lesson tests run child Node processes with Node 22.23.1 and V8 12.4. They assert stable substrings only, because addresses, counts, and exact tiering details can vary. The important facts are visible in release builds: bytecode operands, feedback vectors, slot kinds, lazy allocation messages, and deopt reasons.
function add(a, b) { return a + b;} for (let i = 0; i < 30; i += 1) { add(i, i + 1);} %DebugPrint(add);Node 22.23.1 (V8 12.4)- feedback vector: ... [FeedbackVector]- invocation count: 22- slot #0 BinaryOp BinaryOp:SignedSmall { ... }Node 22.23.1 (V8 12.4)NEVER: feedback metadata is not available in SFIONCE: No feedback vector, but we have a closure feedback cell arrayMANY: slot #0 BinaryOp BinaryOp:SignedSmallfunction add(a, b) { return a + b;} for (let i = 0; i < 100000; i += 1) { add(i, i + 1);} %OptimizeFunctionOnNextCall(add);add(1, 2);add("x", "y");$ node --allow-natives-syntax --no-concurrent-recompilation --trace-opt --trace-deopt probe.js # Node 22.23.1 (V8 12.4)[marking ... reason: hot and stable][completed compiling ...][bailout (kind: deopt-eager, reason: not a Smi): begin. deoptimizing ...]In this V8 build, an integer-only add site prints BinaryOp:SignedSmall. Adding a double widens it to BinaryOp:Number. Boolean or null operands can produce NumberOrOddball. String-only addition can be String, small BigInts can be BigInt64, larger BigInts can be BigInt, and mixed meanings reach Any.
Experiment: make one add site specialize or go generic
PLAYGROUNDUse the buttons to feed one toy slot. Start with number pairs, then try strings or BigInts. Watch the current slot state, the specialization it would suggest, and whether the selected call would pass the current guard.
const slot = createSlot("add slot #0");callAdd(slot, 1, 2); // SignedSmall feedbackcallAdd(slot, 1.5, 2.25); // widens to NumbercallAdd(slot, "1", "2"); // jumps to Any for this siteNonecollecting in the interpreterwould passNo calls yet. Start with a number pair, then try a string pair.
Pick calls to warm this one add site. The slot begins empty, then moves upward as it sees broader types.
add(1, 2)thenadd(3, 4)at the same bytecode site.add(1, 2)thenadd(1.5, 2.25).add(1, 2)thenadd(true, false).add(1, 2)thenadd("1", "2").- A fresh site only ever runs
add("a", "b"). add({ valueOf() { return 1; } }, 2)after number warm-up.
Sort each call pattern by what happens to one feedback slot in the lesson's toy lattice.
What this means for production code
READABILITY FIRSTType feedback is not a reason to write strange code. Most application code should be clear first. The practical rule is narrower: on hot paths you have measured, keep value types and object shapes reasonably consistent. Avoid using one function sometimes as a number path, sometimes as a string path, and sometimes as an object-conversion path if it shows up in a profile.
- Initialize the same object fields in the same order when you create many similar objects.
- Keep hot arithmetic sites numeric if they are meant to be numeric; convert at the edge instead of inside the loop.
- Do not trust a microbenchmark until warm-up values match production values.
- Prefer readable code unless profiling shows the site is hot and unstable.
Tier changes belong to Tiered compilation. IC state machines belong to Inline caches. Speculative guards and deoptimization loops come later. Here, the core idea is the evidence those topics depend on.
Common misconceptions
“Feedback is the same as TypeScript types.”
TypeScript is checked before runtime and erased. Engine feedback is collected while JavaScript executes.
“One function has one type.”
Feedback is attached to operation sites. Two + expressions in one function can have different states.
“Warm-up always makes benchmarks fair.”
Warm-up only helps when it uses representative values and the measured result is still used.
“A generic site is a bug.”
Generic feedback is sometimes the correct model for flexible code. Optimize only measured hot paths.
“Native syntax belongs in app code.”
%DebugPrint and optimization intrinsics are diagnostics for controlled experiments, not production APIs.
| Idea | What it means | Common trap |
|---|---|---|
| Static type | A guarantee before the program runs. | JavaScript engines mostly collect feedback while the program runs. |
| Type feedback | Runtime evidence at a specific operation site. | It can be too narrow when the site has not warmed up yet. |
| Inline cache | The machinery that caches property/call behavior at a site. | IC state details are the next lesson; this lesson focuses on the evidence. |
| Speculative optimization | Compiled code that assumes future values look like past values. | Covered later with deoptimization; here we only show the feedback it consumes. |
Practice exercises
5 EXERCISESWhat state follows SignedSmall when the same add site sees a double?
SignedSmall -> ?
// after one call with 1.5 and 2.25The state becomes Number because a double appeared at the same add site.
What does the toy slot become after a number-warmed + site sees two strings?
add(1, 2);
add("1", "2");It becomes Any, so future optimized plans must be generic unless another site separates the cases.
What does [0] name in the bytecode line?
Add a0, [0][0] is the feedback slot operand for that add bytecode.
Why should a microbenchmark run representative warm-up iterations before timing?
Warm-up lets the interpreter collect type feedback and gives the JIT a chance to tier up before the measured samples.
What should stay reasonably consistent on a measured hot path?
// Fill in the rule:
// On measured hot paths, keep ____ reasonably consistent, but readability first.Keep value types and object shapes reasonably consistent on measured hot paths, but keep readable code first.
Quiz: check your understanding
8 QUESTIONSAnswer by locating the operation site, naming the feedback it saw, and deciding whether the next plan can stay specific.
Question 1 of 8What is type feedback?
Choose an answer to see the explanation.
Question 2 of 8What does this toy feedback snippet print last?
Read the code, then predictlet state = "None"; function merge(previous, observed) { if (previous === "None" || previous === observed) return observed; if (previous === "SignedSmall" && observed === "Number") return "Number"; return "Any"; } state = merge(state, "SignedSmall"); state = merge(state, "Number"); console.log(state);Choose an answer to see the explanation.
Question 3 of 8In V8 bytecode such as
Add a0, [0], what is[0]?Choose an answer to see the explanation.
Question 4 of 8Which call sequence pushes the same
+site to a generic toy state?Choose an answer to see the explanation.
Question 5 of 8What does this warm-up example print first?
Read the code, then predictfunction add(a, b) { return a + b; } for (let i = 0; i < 3; i += 1) add(i, i + 1); console.log("warmed with number calls"); console.log(add(1, 2));Choose an answer to see the explanation.
Question 6 of 8Why can a hot call site become a candidate for inlining?
Choose an answer to see the explanation.
Question 7 of 8Which benchmark habit follows from type feedback?
Choose an answer to see the explanation.
Question 8 of 8What should production code do with this knowledge?
Choose an answer to see the explanation.
Key takeaways
- JavaScript engines collect feedback because source text alone does not reveal stable runtime types.
- In V8, bytecode operands point to feedback slots in a lazily allocated
FeedbackVector. - Binary and compare hints include states such as
SignedSmall,Number,String,BigInt64, andAny. - Property and call feedback feed inline caches and inlining decisions; literal feedback can use allocation sites.
- First runs are cold because they interpret, allocate feedback lazily, collect evidence, and wait for tier-up.
Remember the one-liner.
Feedback vectors are the engine's memory of what each hot bytecode site actually saw.
Up next: Inline caches, where those property and call sites become monomorphic, polymorphic, or megamorphic.