Tracing optimization and deopt
The practitioner toolbox for watching V8 optimise and bail out: --trace-opt, --trace-deopt, --trace-ic, --print-bytecode, --allow-natives-syntax (%OptimizeFunctionOnNextCall, %GetOptimizationStatus), and node --prof / --cpu-prof. How to read a deopt line and name the cause.
A function in your render path is fast for 200ms, then crawls. The CPU profile shows it climbing back to the top of the flame graph every few seconds. You suspect a deopt loop — but suspicion is not a diagnosis. V8 will tell you exactly what happened, at which bytecode offset, and why, if you ask it with the right flags. This lesson is the toolbox for asking. By the end you will know which flag to reach for first, how to read a deopt line to a named cause, and how to drive the optimiser by hand in a controlled experiment.
The mindset: reproduce, trace, read, fix
Optimisation work in V8 is not guesswork. The engine is instrumented from the inside, and Node/d8 expose that instrumentation through flags. The loop is always the same: reproduce the slow path deterministically, run it under the right trace flag, read the events the flag emits, then fix the data or control flow that caused them — never the engine.
These flags work with node (it embeds V8) and with d8 (the standalone V8 shell). For one-off experiments d8 is cleaner because it has no Node noise. For “what is my real app doing” you want node --prof or --cpu-prof.
Tier tracing: —trace-opt and —trace-deopt
--trace-opt prints a line every time a function is selected for optimisation and by which compiler tier. You learn when a function got hot enough to compile and whether it reached Maglev or TurboFan:
$ node --trace-opt app.js
[marking 0x... <JSFunction parseRow> for optimization to MAGLEV, ...]
[completed optimizing 0x... <JSFunction parseRow> (target MAGLEV)]
[marking 0x... <JSFunction parseRow> for optimization to TURBOFAN, ...]--trace-deopt is the one you reach for most. It prints a line every time an optimised function bails back to the interpreter — the event behind almost every “fast then slow” regression:
[deoptimizing (DEOPT eager): begin 0x... <JSFunction sum> (opt #3) @5, FP to SP ...
;;; deoptimize at <app.js:12:14>, reason: Smi, ...Read it left to right: the kind (eager = the guard failed on entry to a checked operation; lazy = the function was invalidated while suspended on the stack; soft = a feedback-poverty bail-out that does not throw away the code), the function and optimisation id, the bytecode offset (@5), the source location, and the reason — here Smi, meaning a value the optimised code assumed was a small integer was not. That one word is the diagnosis: a number overflowed the Smi range or a non-integer arrived.
- --trace-opt
- when + which tier optimised
- --trace-deopt
- deopt kind, offset, reason
- --trace-ic
- IC state transitions per site
- --print-bytecode
- Ignition bytecode dump
- --print-opt-code --code-comments
- TurboFan machine code
- node --prof / --cpu-prof
- sampled / DevTools profile
IC and bytecode tracing
--trace-ic logs every inline-cache state transition at every property-access and call site: the progression from uninitialised (0) to premonomorphic, monomorphic (1), polymorphic (P), and megamorphic (N). When a hot site you expected to be monomorphic shows N, you have found shape divergence — the subject of unit 03. The log includes the map (hidden-class) pointers it saw, so you can count how many distinct shapes hit the site.
--print-bytecode dumps the Ignition bytecode for each compiled function — useful to confirm what the interpreter actually runs (for example, that a property load became a generic LdaNamedProperty rather than something cheaper). --print-opt-code with --code-comments dumps the TurboFan-generated machine code with annotations tying instructions back to source; you reach for it only when you need to confirm an instruction-level assumption, not for everyday work.
Forcing the issue with —allow-natives-syntax
For deterministic experiments, --allow-natives-syntax unlocks %-prefixed intrinsics so you can drive the optimiser by hand instead of waiting for the function to warm up naturally:
function sum(a, b) { return a + b; }
sum(1, 2); // warm up feedback (it has seen Smis)
%OptimizeFunctionOnNextCall(sum); // request optimisation
sum(3, 4); // this call runs optimised code
console.log(%GetOptimizationStatus(sum));%GetOptimizationStatus(fn) returns a bitmask you decode bit by bit. The bits you care about: is optimised, is turbofanned, is maglevved, is interpreted, marked for deoptimisation, is function, and never optimise (the function contains a construct V8 refuses to optimise). A value with the “optimised” and “turbofanned” bits set means it is running TurboFan code; if “marked for deoptimisation” is set, the next call will bail. Other intrinsics round out the kit: %HasFastProperties(obj) tells you whether an object is still in fast (non-dictionary) mode, and %DebugPrint(obj) dumps its full internal layout — the map pointer, the property values, and the array element kind (PACKED_SMI vs HOLEY, and so on).
Profiling the real app
The trace flags answer “why is this function slow”. To find which function is slow first, profile:
node --profwrites anisolate-*.logof sampled stacks;node --prof-process isolate-*.logturns it into a human report with a ticks-by-function breakdown and a section that flags Ignition-vs-optimised time and GC time.node --cpu-profwrites a.cpuprofileyou load straight into the Chrome DevTools Performance panel (or VS Code) for a flame graph — far easier to read than the text profile.
The discipline: profile first to find the dominant cost, then point --trace-deopt / --trace-ic at the function the profile blamed. Tracing the whole app blindly drowns you in lines.
▸Why this works
Why a soft deopt is different: an eager/lazy deopt throws away the optimised code and the function must re-warm and re-compile. A soft deopt happens when the optimised code hits a path it has no feedback for (an insufficient type feedback reason); V8 keeps the code and just collects feedback. A handful of soft deopts at start-up are normal. A repeating eager deopt on the same function at the same offset is the pathology — that is a deopt loop, and the reason word tells you the broken assumption.
A `--trace-deopt` line reads `deoptimizing (DEOPT eager) ... reason: Smi` on a function that sums an array. What is the most likely cause?
You have no idea which function is hot yet. Which tool do you reach for FIRST?
Order the steps of a sound V8 performance diagnosis from scratch.
- 1 Build a deterministic repro that exercises the slow path
- 2 Profile with node --prof / --cpu-prof to find the dominant function
- 3 Run that function under --trace-deopt / --trace-ic to name the cause
- 4 Fix the data or control flow that broke the assumption, then re-verify
- 01Walk through reading a single --trace-deopt line. What are its fields and which one is the diagnosis?
- 02How do you decode %GetOptimizationStatus, and what do you do with %HasFastProperties and %DebugPrint?
- 03What is the difference between node --prof and node --cpu-prof, and where does each fit in the workflow?
Diagnosing V8 performance is a four-step loop: reproduce deterministically, trace, read the events, fix the data or control flow. The flags are the toolbox. --trace-opt reports when a function is optimised and to which tier (Maglev/TurboFan); --trace-deopt reports every bail-out with its kind (eager/lazy/soft), bytecode offset, source location, and reason word — and that reason (Smi, wrong map, lost precision) is the diagnosis. --trace-ic exposes inline-cache state transitions so you can spot shape divergence; --print-bytecode and --print-opt-code --code-comments dump Ignition bytecode and TurboFan machine code for confirmation. Under --allow-natives-syntax you drive the optimiser by hand with %OptimizeFunctionOnNextCall, decode the %GetOptimizationStatus bitmask, and inspect layout with %HasFastProperties and %DebugPrint. To find which function to trace, profile first with node --prof (then --prof-process) or --cpu-prof for a DevTools flame graph. Profile to locate, trace to diagnose, fix upstream of the engine. Now when you see “fast then slow” on a cadence, you know your first move: reach for --cpu-prof, find the function the profile blames, then point --trace-deopt at it — and the reason word will name the broken assumption.
Practice
Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.
Something unclear?
Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.
Apply this
Put this lesson to work on a real build.