Looking inside the engine
Learn how V8 flags, d8, jsvu, bytecode output, trace logs, and visual tools reveal JIT decisions during careful investigation.
- 01Choose a toolPick Node, the d8 shell, or an engine downloaded with jsvu to run a small test file.
- 02Read engine notesRead trace notes, bytecode, and log files as clues about what V8 did, not as a speed score.
- 03Study 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.
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.
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.
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:
- Line 1 makes a function called
add. - Line 2 gives back the sum of its two inputs.
- Line 5 calls
addone thousand times with numbers. This is the warm-up: it makesaddhot. - Line 6 calls
add(2, 3)once more and prints5.
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 V8 for a faster version. Real V8 flags are only for learning and studying problems, never for real users.
script
return a + b;} warmUp(add);prepare(add);optimizeOnNextCall(add);const result = add(2, 3);const status = getStatus(add);console.log(result, status);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.
# 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.jsIn plain words, line by line:
- Line 1 is a comment.
- Line 2 installs jsvu with npm, the tool that installs JavaScript packages.
- Line 3 starts jsvu. It asks which engines you want and downloads them into the folder
~/.jsvu/bin. - Line 5 is a comment.
PATHis the list of folders your terminal searches for programs. - Line 6 prints the version of the V8 shell that jsvu downloaded.
- Line 7 runs
file.jswith 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.
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 ... deoptimizingLine 1 starts Node with two flags and runs file.js:
--trace-optprints a note when V8 builds a faster version of a function.--trace-deoptprints 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 a model of a type change. A real --trace-deopt note is a clue to study, not an alarm for a live site.
script
return a + b;} add(2, 3);add(4, 5);optimizeOnNextCall(add);add(6, 7);console.log(add("2", 1));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.
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:
- Lines 1 to 3 make the same
addfunction. - Line 5 tells V8 to start collecting notes about how
addis used. - Line 6 calls
add(1, 2), so V8 sees two numbers. - Line 7 asks V8 to build the faster version on the next call.
- Line 8 is that next call,
add(2, 3). - 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.
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.
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);function is present, optimized (bit 16 is on)[completed compiling add (lesson model)]
5A string makes + join text instead of adding. The trace note above comes from the model.
The model's status is 17: function is present; optimized (bit 16 is on).
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 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:
- Line 1 is a comment.
- Line 2 saves three kinds of notes:
--log-icfor how property reads went,--log-mapsfor object shapes, and--log-deoptfor dropped fast versions. - Line 3 uses
--profto save a log of where time was spent. - 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.
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.
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]
// ReturnLine 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:
Ldar a1picks upb, the second input, and holds it ready. In bytecode,a0is the first input,a, anda1is the second,b.Add a0, [0]addsato it. The[0]is a slot where V8 keeps notes about what kinds of values it has seen.Returngives the sum back to the caller.
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.
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:
- Measure how long the slow part takes.
- 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.
- Run it with one flag. Write down the engine version and what the trace actually said.
- Compare what you found with the real slowdown.
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.
| Tool | What it is | A good first question for it |
|---|---|---|
| d8 | V8's own shell, which runs a file from the terminal. | What does V8 itself do with this file? |
| jsvu | A tool that downloads recent engines (JavaScript engine Version Updater). | How do I get a recent V8 shell without building it? |
| --trace-opt / --trace-deopt | Flags that print a note when V8 builds or drops a faster version. | Did V8 build or drop a faster version of this function? |
| --print-bytecode | A flag that prints the small steps V8 made from your code. | Which steps, like Ldar, Add, and Return, did my function become? |
| Indicium / Deopt Explorer | Viewers 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.
add(2, 3)andconsole.lognode --print-bytecode file.js%GetOptimizationStatus(add)- The lesson status decoder
--trace-opt --trace-deopt- Number calls followed by
add("2", 1)
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.
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.
| What you saw | What it tells you | What it does not tell you |
|---|---|---|
| A note that a faster version was built | What V8 did in one run. | It does not mean the whole app is fast. |
| A note that a fast version was dropped | The values changed, so V8 went back to the general version. | It is not always a bug or a failure. |
| A status number | A clue from one V8 version. | It is not a fixed rule your app can rely on. |
| Printed machine code | A 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
Run the small program in your head. Type what it prints.
function add(a, b) {
return a + b;
}
for (let index = 0; index < 1000; index += 1) add(index, 1);
console.log(add(2, 3));It prints 5. The warm-up loop prints nothing. The last call adds 2 and 3.
What does the second console.log print, once a string is passed in?
function add(a, b) { return a + b; }
console.log(add(2, 3));
console.log(add("2", 1));The second line prints 21: "2" joined with 1. This is a JavaScript rule. It is the same whether or not the engine built a faster version.
You want to see steps such as Add and Return. Which flag do you add to Node?
Use --print-bytecode. When you can, add --print-bytecode-filter=add so you only see one small function.
Without one flag, Node rejects the percent functions in this lesson. Which flag is it?
Use --allow-natives-syntax, and only while you study a problem. The percent functions can change between V8 versions.
A busy site prints thousands of trace notes. What should you build before you read them?
Build a small reproduction: a tiny file that shows the same problem. Then you can match a function name and a change in values to a trace note.
Your team found a useful trace flag on a laptop. What should stay out of the command that starts the live website?
Keep engine flags out of the command that starts the live website. Use them on your own computer, or in a test run that you control.
Check your understanding
The quiz asks you to tell plain JavaScript results apart from V8 notes that can change between versions.
Question 1 of 7What is d8?
Choose an answer to see the explanation.
Question 2 of 7What does this print?
Read the code, then predictfunction add(a, b) { return a + b; } console.log(add(2, 3));Choose an answer to see the explanation.
Question 3 of 7Why use
--trace-deopt?Choose an answer to see the explanation.
Question 4 of 7What does the lesson decoder print for status 17?
Read the code, then predictconst status = 17; console.log(status & 16 ? "optimized" : "not optimized");Choose an answer to see the explanation.
Question 5 of 7What do you need before you can use the percent functions?
Choose an answer to see the explanation.
Question 6 of 7Which bytecode words does the lesson test check for
add?Choose an answer to see the explanation.
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-optand--trace-deoptprint 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.