completefrontend
Open lab My progress
THE JAVASCRIPT FIELD GUIDE

Looking inside the engine

Learn how V8 flags, d8, jsvu, bytecode output, trace logs, and visual tools reveal JIT decisions during careful investigation.

By the end, you can
  • 01
    Choose a toolPick Node, the d8 shell, or an engine downloaded with jsvu to run a small test file.
  • 02
    Read engine notesRead trace notes, bytecode, and log files as clues about what V8 did, not as a speed score.
  • 03
    Study a problem safelyBuild a small test file, match a trace note to your code, and keep engine flags away from real users.

A scanner for the JavaScript engine

An engine is the program that runs your JavaScript. Chrome and Node both use an engine called V8. While your code runs, V8 makes its own private choices. For example, it may build a faster version of a function that you call many times. The part of V8 that does this is called the JIT compiler (JIT means "just in time": it works while the program runs).

Normally you never see these choices, and that is good. Your program should depend only on what JavaScript promises, not on one engine's private choices. But when you study a speed problem, it helps to see what the engine did. A few V8 tools can show you that.

Definition

Engine tools are extra options, programs, and log files that show what the engine did while it ran a small program. You use them to learn and to find the cause of a problem. You never turn them on for real users.

Most of these tools are flags. A flag is an extra word you type after a command, and it starts with two dashes. For example, in node --trace-opt file.js, the flag is --trace-opt. It asks Node to print notes about what V8 did.

Think of a scanner that a mechanic plugs into a car. The car drives the same way. The scanner only reports what is happening inside the engine.

Real-life analogyA mechanic's scanner

A mechanic plugs a scanner into a car and drives it around the block. The scanner shows which parts got hot and when the car changed gear. The mechanic uses those clues to find the problem. Then the mechanic unplugs the scanner.

In real life: The car still drives the same way
In JavaScript: Your program still prints the same results
In real life: The scanner reports what the engine does
In JavaScript: A flag prints notes about what V8 did
In real life: The mechanic checks one problem at a time
In JavaScript: You test one small piece of code at a time
In real life: The scanner is unplugged before the car is sold
In JavaScript: The flags are taken out before real users run the code

This lesson follows Writing engine-friendly code. Next comes CPU profiling in depth, which shows where your program spends its time. These lessons explain the engine parts you will see here: bytecode and interpreters, tiered compilation, optimizing compilers, and speculation and deoptimization.

Start with a tiny hot function

Hot code is code that runs many times. The engine may spend extra work on hot code to make it faster. We start with a tiny hot function and its printed result, before we add any flag.

A tiny function called many timesPop out in the code editor (opens in a new tab)JavaScript
function add(a, b) {  return a + b;} for (let index = 0; index < 1000; index += 1) add(index, 1);console.log(add(2, 3));

In plain words, line by line:

  1. Line 1 makes a function called add.
  2. Line 2 gives back the sum of its two inputs.
  3. Line 5 calls add one thousand times with numbers. This is the warm-up: it makes add hot.
  4. Line 6 calls add(2, 3) once more and prints 5.

The loop does not promise that V8 will make a faster version of add. It only gives us a small example that runs the same way every time. When you study the engine, change one thing at a time: the number of calls, the kind of values you pass, or the flag you use.

Always keep the last console.log. It shows what JavaScript itself says. If the printed result is wrong, the bug is in your program, not in the engine. If the result is right, any note from the engine is only about how V8 chose to run the code.

The step-through below is a model. Small helper functions such as warmUp and getStatus stand in for the real V8 tools, so you can see each step in order. You will meet the real tools later in this lesson.

Step through a model of asking for a faster add
Step 0 of 7Ready
Your turn: follow the blue line

Step through a model of asking V8 for a faster version. Real V8 flags are only for learning and studying problems, never for real users.

Running in
  1. script
Next: line 1
  return a + b;} warmUp(add);prepare(add);optimizeOnNextCall(add);const result = add(2, 3);const status = getStatus(add);console.log(result, status);
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.

d8 and jsvu

A shell is a small program that runs your code from a terminal. d8 is V8's own shell. It runs a JavaScript file, prints with console.log, and accepts V8 flags. It shows V8 more directly than a browser page does. But it is not the browser your users run.

Download recent engines with jsvuShell
# d8 is V8's own shell. jsvu downloads recent engines.
npm install --global jsvu
jsvu

# Add ~/.jsvu/bin to your PATH, then run the V8 shell that jsvu saved.
~/.jsvu/bin/v8 --version
~/.jsvu/bin/v8 file.js

In plain words, line by line:

  1. Line 1 is a comment.
  2. Line 2 installs jsvu with npm, the tool that installs JavaScript packages.
  3. Line 3 starts jsvu. It asks which engines you want and downloads them into the folder ~/.jsvu/bin.
  4. Line 5 is a comment. PATH is the list of folders your terminal searches for programs.
  5. Line 6 prints the version of the V8 shell that jsvu downloaded.
  6. Line 7 runs file.js with that V8 shell.

jsvu stands for "JavaScript engine Version Updater". It gets you a recent V8 shell without building V8 yourself. Write down the engine version next to every result, because the notes an engine prints can change from one version to the next.

To see which flags your copy accepts, run d8 --help. A command copied from an older or newer V8 may be refused, may have a different name, or may print less. That is normal for these tools. It is one more reason to keep your test file next to your notes.

Trace optimization and deoptimization

Optimization means the engine builds a faster version of a function. It does this after it has watched the function run and has seen what kind of values it gets. For example, if add always gets numbers, V8 can build a fast version made only for numbers.

Deoptimization is the opposite step. The fast version only works for the values the engine expected. When a different kind of value arrives, the engine drops the fast version and goes back to the slower, general version that works for any value.

A trace is a list of notes that the engine prints while it works. Two flags turn on these notes.

Print notes about faster versions in NodeShell
node --trace-opt --trace-deopt file.js

// The words change between V8 versions. Node 22 / V8 12.4 prints:
// marking ... for optimization
// compiling method
// completed compiling
// bailout ... deoptimizing

Line 1 starts Node with two flags and runs file.js:

  • --trace-opt prints a note when V8 builds a faster version of a function.
  • --trace-deopt prints a note when V8 drops one.

Lines 3 to 7 are comments. They show words that Node 22 (with V8 12.4) prints, such as marking ... for optimization, compiling method, completed compiling, and bailout ... deoptimizing. Other V8 versions can use other words. So look for the name of your function, not for one exact phrase.

A trace note is a clue, not a final answer. First check that the function name and the values match your small test file. Then check whether the same thing happens when a real user is waiting. Do not change a whole app because one local trace looks busy.

Here is an example. A product list calls one price function with numbers while you scroll. Once, a bad server reply sends a string instead. A trace can tell you that V8 dropped its fast version at that moment. It cannot tell you what made the page slow. That could be the check on the server reply, drawing the list, the network, or the dropped fast version. A profile and a timed click answer that question; the next lesson covers profiles.

The step-through below shows a type change. It is a model: the helper optimizeOnNextCall stands in for a real V8 tool. The printed 21 is real JavaScript.

Step through numbers, then a string
Step 0 of 6Ready
Your turn: follow the blue line

Step through a model of a type change. A real --trace-deopt note is a clue to study, not an alarm for a live site.

Running in
  1. script
Next: line 1
  return a + b;} add(2, 3);add(4, 5);optimizeOnNextCall(add);add(6, 7);console.log(add("2", 1));
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.
Left: add(6, 7) goes into the add function and gives 13, using the fast version made for numbers. Right: add("2", 1) goes into the same add function and gives "21", so the engine goes back to the general version.add(6, 7)add13fast number versionadd("2", 1)add"21"back to the general version
add(6, 7) uses the fast version made for numbers. The string in add("2", 1) makes + join text, so the engine drops that version, and the program still prints 21.

Percent functions and status bits

V8 has some hidden functions whose names start with a percent sign, such as %GetOptimizationStatus. They are called V8 intrinsics. Here is what to know about them:

  • They are not part of JavaScript. Browsers reject them.
  • They only work when you start Node or d8 with the flag --allow-natives-syntax.
  • Their names and what they return can change from one V8 version to the next.
Ask V8 to build a faster add, then ask how it wentJavaScript
function add(a, b) {
  return a + b;
}

%PrepareFunctionForOptimization(add);
add(1, 2);
%OptimizeFunctionOnNextCall(add);
add(2, 3);
console.log(%GetOptimizationStatus(add));

In plain words, line by line:

  1. Lines 1 to 3 make the same add function.
  2. Line 5 tells V8 to start collecting notes about how add is used.
  3. Line 6 calls add(1, 2), so V8 sees two numbers.
  4. Line 7 asks V8 to build the faster version on the next call.
  5. Line 8 is that next call, add(2, 3).
  6. Line 9 asks V8 for a status number that describes add, and prints it.

The status number packs several yes-or-no facts into one number. Each fact is one bit: one binary digit that is either on or off. The bits are worth 1, 2, 4, 8, 16, and so on. For example, 17 is 16 + 1, so the bit worth 16 and the bit worth 1 are on, and all the others are off.

This lesson checks just one fact, on Node 22. After lines 5 to 8, the status always has the bit worth 16 turned on. We call that bit "optimized". This is not a promise from V8 that 16 always means this. We also do not decode the other bits.

To test one bit, use status & 16. The & operator keeps only the bits that are on in both numbers. If the answer is 16, that bit is on. If it is 0, the bit is off. The & is ordinary JavaScript. The meaning "16 is optimized" comes only from our test on one V8 version.

The status number 17 splits into 16 plus 1. The check status & 16 keeps only the 16, which is not zero, so the lesson's decoder prints optimized.status1716+117 is 16 + 1status & 16optimized16 is not zero
The status 17 is 16 + 1. The check status & 16 keeps only the 16 part, and that is not zero, so the decoder prints optimized.

Model a status decoder

A decoder turns a status number into words. The playground below is a model of that. It does not look inside your browser's engine. It gives the same answer every time, so you can compare choices. You can turn three things on or off:

  • the warm-up calls with numbers,
  • the request to build a faster version on the next call,
  • a later call with a string.

Turn one choice on or off. Then read the status and the trace note that the model prints. With the first two choices on, the status is 17, which has the bit worth 16 on. When a string arrives later, + joins text instead of adding, and the model prints a note about dropping the fast version.

Playground: turn a status number into words
The steps the model followsJavaScript
function add(a, b) {  return a + b;} warmUp(add);prepare(add);optimizeOnNextCall(add);const result = add(2, 3);const status = getStatus(add);console.log(result, status);
What the model reportsstatus 17
status in wordsfunction is present, optimized (bit 16 is on)

[completed compiling add (lesson model)]

real JavaScript result5

A string makes + join text instead of adding. The trace note above comes from the model.

Try it yourself

The model's status is 17: function is present; optimized (bit 16 is on).

This runs the lesson's small model. It does not look inside your browser's engine, and real V8 status numbers can mean other things.

Log viewers: Indicium and Deopt Explorer

Trace notes are accurate, but there can be thousands of them. A log file is a file where the engine saves its notes instead of printing them. Two tools draw log files as pictures and lists you can click through:

  • Indicium, a log viewer made by the V8 team.
  • Deopt Explorer, an extension for the VS Code editor, made by Microsoft.

Use them only after you have a small test file and know which function you are looking for.

Save engine notes to log filesShell
# Save V8 notes to log files while you study a small test file.
node --log-ic --log-maps --log-deopt file.js
node --prof file.js

# Open the log in a viewer such as Indicium or Deopt Explorer.
# A viewer helps you look through the notes. It does not change the program.

In plain words, line by line:

  1. Line 1 is a comment.
  2. Line 2 saves three kinds of notes: --log-ic for how property reads went, --log-maps for object shapes, and --log-deopt for dropped fast versions.
  3. Line 3 uses --prof to save a log of where time was spent.
  4. Lines 5 and 6 are comments: you open the log in a viewer, and the viewer does not change your program.

These flags only record what happened in one run. A viewer helps you move around those notes. Neither one proves that a note is the reason a page felt slow.

Open the log in a viewer that reads that kind of file. Show only the function from your test file, then go back to your source code. A picture is easier to scan than text. But you still need three things: the values you passed, the note the engine printed, and a slowdown you can measure.

When a viewer shows many notes, start small. Search for add, or the function from your test file. Find the call where the values changed, and look at the notes around it. A colorful timeline is not a list of things to rewrite. It only makes the notes easier to move through.

Print bytecode

Before V8 runs a function, it turns your source code into bytecode: a list of small, simple steps. A part of V8 called the interpreter then runs those steps one by one. Printing the bytecode shows you those steps. It does not promise that every call runs exactly these steps, because a faster version may take over later.

A small function to turn into bytecodePop out in the code editor (opens in a new tab)JavaScript
function add(a, b) {
  return a + b;
}

console.log(add(2, 3));

Line 1 makes add. Line 2 adds the two inputs. Line 5 calls it and prints 5. The function is tiny on purpose, so each bytecode step is easy to match to the source.

Print the bytecode for add onlyShell
node --print-bytecode --print-bytecode-filter=add file.js

// The numbers in this output change between V8 versions. These step names
// stay the same for this tiny function:
// Ldar a1
// Add a0, [0]
// Return

Line 1 has two flags. --print-bytecode prints bytecode, and --print-bytecode-filter=add keeps only the bytecode for add. The lesson test runs this on Node 22 and checks that the output has Add and Return. It also has Ldar, but other numbers in the listing can change between versions.

Read the three steps of the listing in order:

  1. Ldar a1 picks up b, the second input, and holds it ready. In bytecode, a0 is the first input, a, and a1 is the second, b.
  2. Add a0, [0] adds a to it. The [0] is a slot where V8 keeps notes about what kinds of values it has seen.
  3. Return gives the sum back to the caller.
The source line return a + b; turns into three bytecode steps for add: Ldar a1 picks up b, Add a0, [0] adds a to it, and Return gives back the sum.return a + b;your source lineLdar a1pick up bAdd a0, [0]add a to itReturngive back the sumbytecode for add
Line 2, return a + b;, becomes three small steps. a0 is the first parameter, a, and a1 is the second, b.

Print machine code carefully

You can go one level lower than bytecode. V8 can print the machine code of a faster version: the instructions the computer's processor runs directly. This output is long, different for every V8 version, and full of details that teach you nothing about JavaScript itself.

Print the machine code of a faster addShell
node --print-opt-code --print-opt-code-filter=add file.js

// This prints a lot of low-level output. Its layout and memory addresses
// change between Node and V8 versions, so don't copy it into tests.

Line 1 prints the machine code, but only for add. Lines 3 and 4 are comments that warn you: the layout and the memory addresses change from one Node version to the next.

Read this output only after you understand the source, the bytecode, and the trace. Compare it only with output from the same run. Do not compare memory addresses, and do not paste your output into notes as if every computer prints the same thing.

For most app work, bytecode, trace notes, and a CPU profile are better first tools. Print machine code only when you have one narrow question that it can answer.

A simple rule helps. If you cannot say what question about your source code the machine code will answer, do not print it yet. A trace may already show that the engine dropped a fast version. A profile may show that the function is not even slow. Low-level output is worth reading only when it helps you make a real choice.

Steps for studying a slow spot

Start from something a user can notice, such as a slow search or a list that stutters while scrolling. Then follow these steps:

  1. Measure how long the slow part takes.
  2. Copy just the code you suspect into a small file. This small file is called a reproduction: it shows the same problem on its own.
  3. Run it with one flag. Write down the engine version and what the trace actually said.
  4. Compare what you found with the real slowdown.
Four steps in a row. 1, measure what the user waits for. 2, copy the code into a small file.js. 3, run one flag such as --trace-deopt with one input change. 4, compare: does it matter to the user? Then take the flags out.1. Measuretime what theuser waits for2. Small filecopy the codeinto file.js3. One flag--trace-deoptone input change4. Comparedoes it matterto the user?then take the flags out
Work left to right and change one thing at a time. When you are done, take the flags out of every command you ship.

The table lists the tools from this lesson. Read each row as: the tool, what it is, and the first question it can help you answer.

Pick the smallest tool that answers your question
ToolWhat it isA good first question for it
d8V8's own shell, which runs a file from the terminal.What does V8 itself do with this file?
jsvuA tool that downloads recent engines (JavaScript engine Version Updater).How do I get a recent V8 shell without building it?
--trace-opt / --trace-deoptFlags that print a note when V8 builds or drops a faster version.Did V8 build or drop a faster version of this function?
--print-bytecodeA flag that prints the small steps V8 made from your code.Which steps, like Ldar, Add, and Return, did my function become?
Indicium / Deopt ExplorerViewers that show V8 log files as pictures and lists.Where in my code did these logged notes happen?

Next, change one input. For example, call add with numbers, and then once with a string. If a trace note appears, compare it with a profile or with a real click. At the end, take the flags out. Keep your app code simple, unless a measurement shows that a change really makes it faster.

Also write down when nothing happened. "No trace note appeared for this input on this engine" is useful, because it stops you from trying the same guess again. Keep these together:

  • the test file,
  • the full command you ran,
  • the engine version,
  • the values you passed, in order,
  • the printed result,
  • the slowdown you measured.

That record is worth more than a single screenshot of a log.

Where can this example run?
  • add(2, 3) and console.log
  • node --print-bytecode file.js
  • %GetOptimizationStatus(add)
  • The lesson status decoder
  • --trace-opt --trace-deopt
  • Number calls followed by add("2", 1)
Try it yourself
0 of 6 correct

Put each card where it belongs: plain JavaScript that runs in a browser, something that only works in Node or d8, or this lesson's model.

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

Common mistakes

  • "A trace flag makes my app faster." It only prints notes about what the engine did. Printing them can even slow the program down.
  • "Dropping a fast version is always bad." It can be the normal thing to do when the values change.
  • "A status number means the same thing everywhere." Its meaning can change from one V8 version to the next.
  • "Bytecode is how the code always runs." During a run, the engine can switch to a faster version, or back again.

The table below lists things you may see. Read each row as: what you saw, what it tells you, and what it does not tell you.

Do not read too much into one note
What you sawWhat it tells youWhat it does not tell you
A note that a faster version was builtWhat V8 did in one run.It does not mean the whole app is fast.
A note that a fast version was droppedThe values changed, so V8 went back to the general version.It is not always a bug or a failure.
A status numberA clue from one V8 version.It is not a fixed rule your app can rely on.
Printed machine codeA printout from one V8 version on one computer.It does not tell you what JavaScript itself promises.

Remember the main rule. JavaScript must always give the same results: what your program prints, returns, throws, and changes. The engine may run your code in different ways inside, but it must keep those results the same.

Practice exercises

Exercise 1 · Warm-upPredict the tiny function

Run the small program in your head. Type what it prints.

Starter codePop out in the code editor (opens in a new tab)JavaScript
function add(a, b) {
  return a + b;
}

for (let index = 0; index < 1000; index += 1) add(index, 1);
console.log(add(2, 3));

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

    Exercise 2 · Warm-upPredict the call with a string

    What does the second console.log print, once a string is passed in?

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    function add(a, b) { return a + b; }
    console.log(add(2, 3));
    console.log(add("2", 1));

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

      Exercise 3 · PracticeName the bytecode flag

      You want to see steps such as Add and Return. Which flag do you add to Node?

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

        Exercise 4 · PracticeTurn on the percent functions

        Without one flag, Node rejects the percent functions in this lesson. Which flag is it?

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

          Exercise 5 · ChallengeGet ready to read a trace

          A busy site prints thousands of trace notes. What should you build before you read them?

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

            Exercise 6 · ChallengeApply this to a real website

            Your team found a useful trace flag on a laptop. What should stay out of the command that starts the live website?

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

              Check your understanding

              The quiz asks you to tell plain JavaScript results apart from V8 notes that can change between versions.

              Engine tools quiz · 7 questionsScore: first tries count
              1. Question 1 of 7What is d8?

                Choose an answer to see the explanation.

              2. Question 2 of 7What does this print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                function add(a, b) { return a + b; }
                console.log(add(2, 3));

                Choose an answer to see the explanation.

              3. Question 3 of 7Why use --trace-deopt?

                Choose an answer to see the explanation.

              4. Question 4 of 7What does the lesson decoder print for status 17?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                const status = 17;
                console.log(status & 16 ? "optimized" : "not optimized");

                Choose an answer to see the explanation.

              5. Question 5 of 7What do you need before you can use the percent functions?

                Choose an answer to see the explanation.

              6. Question 6 of 7Which bytecode words does the lesson test check for add?

                Choose an answer to see the explanation.

              7. Question 7 of 7You see one note that V8 dropped a fast version. What should you do next?

                Choose an answer to see the explanation.

              Key takeaways

              • d8 is V8's own shell, and jsvu downloads recent engines for you.
              • --trace-opt and --trace-deopt print notes when V8 builds or drops a faster version. Use them on a small test file.
              • Percent functions need --allow-natives-syntax, and they can change with each V8 version.
              • Indicium and Deopt Explorer help you look through V8 log files.
              • Bytecode and machine code printouts are clues from one version, not rules that hold everywhere.
              • Use engine flags only to learn and to study problems. Never turn them on for real users.

              Remember the one-liner.
              Engine tools are a scanner: they show what one engine chose to do, and then you check whether it matters to the user.

              Coming next: CPU profiling in depth, which shows where your program spends its time.

              CompleteFrontend Clear concepts. Working examples.