completefrontend
Open lab My progress
THE JAVASCRIPT FIELD GUIDE

Type feedback & feedback vectors

Learn how JavaScript engines collect runtime type feedback in vectors and use warm-up evidence to specialize hot code safely.

By the end, you can
  • 01
    Explain how the engine collects notesSay how an engine writes down the values each operation really sees while the code runs, because JavaScript code has no fixed types.
  • 02
    Read slots and their statesConnect a slot number in the bytecode, like [0], to a slot in a FeedbackVector, and name states like SignedSmall, Number, and Any.
  • 03
    Warm up code honestlyExplain why the first runs are slow, why one new kind of value widens a slot, and how to warm up a speed test so its timings are fair.

The engine learns from the values your code really uses

A JavaScript engine is the program inside a browser or Node that runs your code. In JavaScript, a variable has no fixed type. The same a + b can add two numbers on one call and join two strings on the next.

So the engine can't know from the code alone what a + b will get. Instead, it watches. Each time the code runs, the engine writes a short note: "this + got two small whole numbers." Later, it uses those notes to make faster code for the values it has seen.

Definition

Type feedback is the set of notes an engine keeps about the values one operation has really seen. An operation here is one place in the code that does one thing, like one +, one user.name, or one function call. Each operation gets its own notes.

This lesson comes after tiered compilation and before inline caches. Here you learn where the notes are kept and how they change. Later lessons show how the engine uses the notes to make fast code, and what it does when a guess turns out wrong.

Real-life analogyA barista remembers regular orders

Picture a cafe where each drink station keeps a notebook of what people order there. Every morning, the espresso station only gets double shots. So the barista gets that station ready for double shots before anyone asks. Then one customer orders a smoothie at the same station. The quick setup no longer fits, so the barista goes back to the slower, general way.

In real life: The barista watches real customers
In JavaScript: The engine watches real calls
In real life: Each drink station has its own notebook
In JavaScript: Each operation has its own feedback slot
In real life: Espresso is always ordered here, so it is made ready early
In JavaScript: The same kind of value every time lets the engine make a fast path
In real life: One smoothie order changes the plan
In JavaScript: A new kind of value widens the notes and can undo the fast path

These lessons help if you want to look back: data types, numbers in depth, type coercion, bytecode and interpreters, and measuring performance.

Collecting types while the code runs

THE INTERPRETER WATCHES

At first, the notes are empty. The engine starts every function in its interpreter: the part that runs bytecode, a simple list of steps made from your code. While it runs each step, it writes down what that step saw.

In V8, the engine in Chrome and Node, the interpreter is called Ignition. V8's faster compilers, Maglev and TurboFan, read the notes later. But first, the notes have to be filled in with real values.

The code below is a small model of this, written in plain JavaScript. It keeps notes for one +. The names of the states, like SignedSmall, are V8's real names:

  • SignedSmall: small whole numbers, like 1 and 2.
  • Number: any numbers, including decimals like 1.5.
  • String: only strings.
  • Any: a mix, so anything can come.
A small model that keeps notes for one +Pop out in the code editor (opens in a new tab)JavaScript
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 by line:

  1. Line 1 makes one empty slot. A slot is the place where the notes for one operation are kept.
  2. Lines 7 to 12, classify, look at the two values of one call and name their kind.
  3. Lines 14 to 18, merge, mix the new kind with the old notes. The notes only get wider, never narrower.
  4. Lines 20 to 27, specialize, name the fast plan that would fit the current state.
  5. Lines 29 to 38, callAdd, do one addition and update the slot.
  6. Lines 44 to 46 call the same slot three times and print the state after each call.

The first line printed is SignedSmall -> int add fast path. The decimals 1.5 and 2.25 widen the state to Number. Then two strings arrive. A + with strings joins them instead of adding, so this slot becomes Any.

One add slot after three calls. After add(1, 2) it says SignedSmall. After add(1.5, 2.25) it says Number. After add of two strings it says Any.add(1, 2)SignedSmallint add fast pathadd(1.5, 2.25)Numbernumber add fast pathadd("1", "2")Anygeneric add path
The same slot after each call. Each new kind of value moves the slot to a wider state, and it never moves back.
Step through: whole numbers, then decimals
Step 0 of 5Ready
Your turn: follow the blue line

Step through a toy slot as it takes notes from real calls. The state goes from None, to SignedSmall, to Number.

Running in
  1. script
Next: line 1
callAdd(slot, 1, 2);callAdd(slot, 3, 4);callAdd(slot, 1.5, 2.25);
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

Each slot belongs to one place in the code. A different + in the same function has its own slot. That slot could stay String or SignedSmall forever. The engine makes fast code for each place, not for the + sign everywhere.

Feedback vectors and slots

INSIDE V8

In V8, each function keeps all its slots together in one list. That list is called a FeedbackVector. There are three parts to know:

  • The JSFunction is the function object your code holds.
  • The FeedbackCell is a small holder that the function points to.
  • The FeedbackVector is the full list of slots. The cell points to it once it exists.

V8 does not make the full vector right away. It waits until the function has run enough times. Many functions run only once or never, so this saves memory. V8 calls this lazy feedback allocation: "lazy" means "only when it is needed", and "allocation" means setting memory aside.

Where this comes from

V8's public notes on how functions are built describe JSFunction, FeedbackCell, and FeedbackVector. The V8 Lite blog post explains why V8 makes feedback vectors only after a function has run a while: it saves memory for functions that never get busy.

You can see the slot number in the bytecode. Node prints the bytecode of a function when you pass it the flag --print-bytecode:

The add step names its slotShell
$ 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                Return

Look at line 5, Add a0, [0]. The [0] is not an array. It is the number of the feedback slot for this add step. Once the function has run enough, slot 0 of its vector holds the notes for this +.

The bytecode line Add a0, [0]. An arrow labelled slot 0 points to the feedback vector of add, which holds slot #0 with SignedSmall.Add a0, [0]bytecode of addslot 0#0 SignedSmallvectorthe feedback vector of add
The [0] is not an array. It is the number of a slot in the feedback vector of add. After calls with small whole numbers, slot 0 says SignedSmall.

The states of one add slot, from narrow to wide:

  1. None
  2. SignedSmall
  3. Number
  4. NumberOrOddball
  5. Any

NumberOrOddball means numbers mixed with true, false, null, or undefined. V8 calls those four values "oddballs". Strings and BigInts have their own states on the way to Any: String, StringOrStringWrapper, BigInt64, and BigInt.

Notes for calls, properties, operators, loops, and literals

KINDS OF SLOTS

A slot is not only for +. V8 has slots for many operations: calls, reading and writing properties, math and comparisons, for...in loops, instanceof, typeof, copying objects, and literals like {} and []. You don't need to learn the list. It shows that the notes say much more than "the type of a variable".

Read each row of the table as: "for this operation, the slot writes down this, and the engine uses it like this."

What different kinds of slots write down
Which operationWhat the slot writes downHow the engine uses it
Math and comparisons, like a + b or a < bThe kind of values it got, like SignedSmall, Number, NumberOrOddball, String, BigInt64, BigInt, or Any.It picks fast math for numbers, and knows whether + adds numbers or joins strings.
Reading or writing a property, like user.nameThe shapes of the objects it saw, and how to reach the property in each shape.It feeds inline caches. The next lesson, Inline caches, explains them.
A function call, like fn(value)Which function ran, and roughly how often.If the same function always runs, the engine may copy its body into the call.
for...in loops and instanceofThe object shapes the loop walked through, or the class that was checked.Repeated loops and checks run faster while the shapes stay the same.
Literals, like {} and []Where the object or array was made, and a ready-made model of it.The next object from the same line can copy that model instead of starting from nothing.
Reading a property from objects of the same shapePop out in the code editor (opens in a new tab)JavaScript
function readName(user) {  return user.name;} console.log(readName({ name: "Ada", role: "admin" }));console.log(readName({ name: "Grace", role: "engineer" }));

Line 2 reads user.name. Lines 5 and 6 call it with two objects that have the same properties in the same order, so it prints Ada, then Grace. The slot for line 2 writes down the object's shape: V8's record of which properties an object has and where each one is stored. V8 calls a shape a map.

These notes feed the inline caches you'll meet in the next lesson. That lesson also explains the names monomorphic (one shape), polymorphic (a few shapes), and megamorphic (many shapes).

A call that always runs the same functionPop out in the code editor (opens in a new tab)JavaScript
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);

The call fn(value) on line 2 runs inc every time. Lines 15 and 16 print target: inc and calls: 3, to show in plain JavaScript what the call slot would note: the same function, called again and again.

When a call always runs the same function, the faster compiler may inline it: copy the function's body into the place of the call, so no call is needed. Slots for literals remember where an object or array was made, so the next {} or [] from that line does not start from nothing.

Why the first runs are slow, and what warm-up means

COLD AND HOT

A function that has just started is cold. A function that has run many times is hot. A cold function pays for a few things first:

  • Turning the code into bytecode.
  • Running slowly in the interpreter.
  • Making the feedback vector, once it has run enough.
  • Filling the slots with notes.
  • Waiting until the engine decides it is hot enough for a faster compiler.

So the first runs are the least typical runs. If you time only them, you mostly time this setup work. Running the code many times before you start timing is called warm-up.

Warm up with typical values before timingPop out in the code editor (opens in a new tab)JavaScript
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));

Lines 5 and 6 call add 10,000 times with numbers. Line 9 prints warmed with 10000 number calls, and line 10 prints 3. The loop does not make add faster by itself. It gives the engine time to collect notes about numbers. If add stays hot, the engine can then make fast code for numbers.

Warm-up only helps if the values match the ones you time. The fast code has a guard: a quick check in front that asks "are these the values I was made for?" If the answer is no, the engine leaves the fast code. That is called a deopt, short for deoptimization.

Step through: a fast plan, then strings fail the guard
Step 0 of 6Ready
Your turn: follow the blue line

Step through a toy engine: the notes give a fast plan with a guard. Strings fail the guard, and the answer is still correct: "34".

Running in
  1. script
Next: line 1
callAdd(slot, 1, 2);const plan = specialize(slot.state);callOptimizedAdd(plan, 3, 4);callOptimizedAdd(plan, "3", "4");
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.
A guard checks each call. For 3 and 4 it answers yes, so the int add fast path gives 7. For the strings 3 and 4 it answers no, so the engine leaves the fast plan, uses the generic add and gives the string 34.3, 4guard:ints?yesint addfast path7"3", "4"guard:ints?nogeneric addleaves the fast plan"34"
The fast plan has a guard in front: "are both values small whole numbers?" If not, the engine leaves the fast plan, and the answer is still correct.

Suppose a test warms up with numbers and then times strings. It measured the switch from one kind of value to another. Sometimes you want exactly that. Most of the time it is an accident. Use the habits from Measuring performance:

  • Warm up first.
  • Use each result, so the engine can't skip the work.
  • Take many timings, not one.
  • Report the middle value (the median) or how much the timings vary.

Real V8 output from Node 22

V8 12.4

So far, the code was a model. The lesson's tests also run real Node 22, which uses V8 12.4, and check its output. Small details like memory addresses, counts, and exact timing change between versions. So the tests only check short, stable pieces of text.

The programs below use %DebugPrint and %OptimizeFunctionOnNextCall. These are V8 test commands, not JavaScript. They only work when you start Node with the flag --allow-natives-syntax, so these blocks don't run here.

Ask V8 to print a function's feedback vectorJavaScript
function add(a, b) {  return a + b;} for (let i = 0; i < 30; i += 1) {  add(i, i + 1);} %DebugPrint(add);
Part of the output: Node 22 (V8 12.4)Text
Captured on Node 22 (V8 12.4)- feedback vector: ... [FeedbackVector]- invocation count: 22- slot #0 BinaryOp BinaryOp:SignedSmall { ... }

Lines 5 and 6 of the program call add 30 times with whole numbers. Line 9 prints what V8 knows about add. In the output, line 2 shows that a feedback vector exists. Line 4 shows slot 0, the +, with the state SignedSmall.

Three functions, three amounts of feedbackText
Captured on Node 22 (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:SignedSmall

This output comes from three functions. never was never called, once was called once, and many was called 30 times:

  • Line 2: never has no feedback information at all yet.
  • Line 3: once has a feedback cell, but no vector yet.
  • Line 4: many has a vector, and slot 0 says SignedSmall.
Three functions. never was called 0 times and has no feedback metadata. once was called 1 time and has only a feedback cell. many was called 30 times and has a full feedback vector.neverno metadata0 callsoncecell only1 callmanyvector30 callsmore calls, more feedback storage
The three functions from the probe. A function gets a full feedback vector only after it has run enough times. Until then, V8 saves memory.
Make add fast, then send it stringsJavaScript
function 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");
Part of the trace: Node 22 (V8 12.4)Text
$ node --allow-natives-syntax --no-concurrent-recompilation --trace-opt --trace-deopt probe.js   # Node 22 / V8 12.4 capture[marking ... reason: hot and stable][completed compiling ...][bailout (kind: deopt-eager, reason: not a Smi): begin. deoptimizing ...]

The program warms add with numbers on lines 5 and 6. Line 9 asks V8 to make fast code on the next call. Line 10 uses it with numbers, and line 11 sends strings. In the trace, line 2 says V8 picked add because it was hot and stable. Line 4 shows the deopt. The reason is not a Smi: a Smi, short for "small integer", is V8's fast form for small whole numbers, and a string is not one.

Here is what this V8 version prints for one add slot, depending on the values it saw:

  • Only small whole numbers: BinaryOp:SignedSmall.
  • Whole numbers, then one decimal: BinaryOp:Number.
  • Booleans or null: NumberOrOddball.
  • Only strings: String.
  • Small BigInts: BigInt64. Larger BigInts: BigInt.
  • A mix of different meanings: Any.

Try it: keep one add slot fast, or make it general

PLAYGROUND

Each button sends one call to the same toy slot. Start with whole numbers, then try strings or BigInts. Watch three things:

  • The slot's current state.
  • The fast plan that state would allow.
  • Whether the guard would let the call you picked use that plan.
Send calls to one add slot
The calls this playground modelsJavaScript
const slot = createSlot("add slot #0");callAdd(slot, 1, 2);        // small whole numbers: SignedSmallcallAdd(slot, 1.5, 2.25);   // decimals: wider, NumbercallAdd(slot, "1", "2");    // strings too: Any
Slot state0 calls
current stateNone
fast plancollecting in the interpreter
guard, same call againwould pass

No calls yet. Start with whole numbers, then try strings.

Try it yourself

Press a button to send one call to this add slot. The slot starts empty, then gets wider as wider kinds of values come.

This is a small model, not a view inside a real engine. It keeps notes on the calls you pick and shows the fast plan an engine could try.
Stays fast or goes general?
  • add(1, 2) then add(3, 4) at the same +.
  • add(1, 2) then add(1.5, 2.25).
  • add(1, 2) then add(true, false).
  • add(1, 2) then add("1", "2").
  • A new + that only ever runs add("a", "b").
  • add({ valueOf() { return 1; } }, 2) after number calls.
Try it yourself
0 of 6 correct

Sort each set of calls by what happens to one slot in the lesson's model.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

What this means for your everyday code

CLEAR CODE FIRST

Type feedback is not a reason to write strange code. Most code should be easy to read first. The useful rule is small. It applies only to hot paths: the code that runs most often, which you found by measuring. On those paths, keep the kinds of values and the shapes of objects the same from call to call.

  • When you make many similar objects, give them the same properties in the same order.
  • If a hot + is meant for numbers, turn text into numbers before the loop, not inside it.
  • Don't trust a small speed test until its warm-up values match the real values.
  • Keep code easy to read, unless measuring shows a place is hot and its values keep changing kind.
Other lessons cover the next steps

Moving between the engine's speed levels is in Tiered compilation. The states of a property or call cache are in Inline caches. Guards and deopts get their own lessons later. This lesson is about the notes that all of them read.

Common misconceptions

"Feedback is the same as TypeScript types."

TypeScript types are checked before the code runs, and then removed. Type feedback is collected by the engine while the JavaScript runs.

"One function has one type."

Notes belong to each operation. Two + signs in one function can have different states.

"Warm-up always makes speed tests fair."

Warm-up only helps when it uses typical values and the timed results are still used.

"A slot in the Any state is a bug."

For code that is meant to take many kinds of values, Any is the right state. Only change code you measured as hot.

"V8 test commands belong in app code."

%DebugPrint and similar commands are for experiments and tests, not for real apps.

Read each row of the table as: "this word means this, and here is the mix-up to avoid."

Words that are easy to mix up
WordWhat it meansThe mix-up to avoid
Static typeA type that is fixed and checked before the program runs.JavaScript engines don't get these from your code. They collect notes while it runs.
Type feedbackThe notes about the values one operation has really seen.Early on, the notes can be too narrow, because the code hasn't warmed up yet.
Inline cacheA quick memory at one property read or call: "last time it was this shape, so look here".The next lesson covers inline caches. This lesson is about the notes they read.
Speculative optimizationFast code that guesses future values will look like past values.Later lessons cover it, with deopts. Here you only see the notes it reads.

Practice exercises

5 EXERCISES
Exercise 1 · Warm-upName the wider number state

A slot is in the SignedSmall state. Then the same + gets two decimals. What is the new state?

Starter codePop out in the code editor (opens in a new tab)JavaScript
SignedSmall -> ?
// after one call with 1.5 and 2.25

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · PracticePredict what one string call does

    A + slot has only seen numbers. Then it gets two strings. What state does the toy slot move to?

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    add(1, 2);
    add("1", "2");

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeRead the bytecode

      What does [0] mean in this line of bytecode?

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      Add a0, [0]

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeExplain warm-up

        Why should a small speed test run the code many times with typical values before it starts timing?

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeSay the everyday rule

          On a hot path you have measured, what should stay the same from call to call?

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          // Fill in the rule:
          // On measured hot paths, keep ____ reasonably consistent, but readability first.

          Answer, then press Check. Spacing and letter case don’t matter.

            Quiz: check your understanding

            8 QUESTIONS

            For each question: find the operation, list the values it saw, and decide whether its fast plan can stay narrow.

            Lesson quiz · 8 questionsScore: first tries count
            1. Question 1 of 8What is type feedback?

              Choose an answer to see the explanation.

            2. Question 2 of 8What does this toy feedback snippet print last?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              let 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.

            3. Question 3 of 8In V8 bytecode such as Add a0, [0], what is [0]?

              Choose an answer to see the explanation.

            4. Question 4 of 8Which calls move the same + slot to Any in the lesson's model?

              Choose an answer to see the explanation.

            5. Question 5 of 8What does this warm-up example print first?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              function 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.

            6. Question 6 of 8Why might the engine copy a function's body into a busy call (inline it)?

              Choose an answer to see the explanation.

            7. Question 7 of 8Which speed-test habit follows from type feedback?

              Choose an answer to see the explanation.

            8. Question 8 of 8What should everyday code do with this knowledge?

              Choose an answer to see the explanation.

            Key takeaways

            • The code alone doesn't say which kinds of values will come, so the engine watches and writes notes.
            • In V8, a step in the bytecode names its slot, like [0]. The slots live in a FeedbackVector, made only once a function has run enough.
            • An add slot can say SignedSmall, Number, String, BigInt64, Any, and more. It only gets wider.
            • Property and call slots feed inline caches and inlining. Literal slots remember where objects and arrays are made.
            • First runs are slow: the engine interprets, makes the vector, fills the slots, and waits before it makes faster code.

            Remember the one-liner.
            A feedback vector holds the engine's notes on what each step of a busy function really saw.

            Up next: Inline caches, where property and call slots become monomorphic, polymorphic, or megamorphic.

            CompleteFrontend Clear concepts. Working examples.