A deoptimization (deopt) occurs when a JavaScript engine's optimizing compiler, like V8's TurboFan, has to abandon its generated fast machine code because one of its speculative assumptions about the code's behavior turns out to be wrong during execution .
JavaScript engines are speculative optimizers. They observe how code runs in the interpreter or baseline compiler, collect profiling data (like object shapes or variable types), and then generate aggressively optimized machine code based on the assumption that these observed patterns will continue . When an assumption breaks, the optimized code is no longer valid, and the engine must 'deoptimize'—roll back to the slower, but correct, interpreted bytecode to continue execution safely . This process is also sometimes called an 'OSR exit' (On-Stack Replacement) . Deopts are a critical part of the JIT's safety mechanism, ensuring correctness while still aiming for performance.
Type Changes (Most Common): The most frequent cause is a change in the data type of a variable or parameter. For example, a function like add(a, b) that was always called with integers might be optimized for integer addition. If it's later called with a string, the integer-optimized code breaks, causing a deopt .
Object Shape Changes: Optimized code relies on an object's Hidden Class (or Map/Shape) being consistent. If an object gains a new property (e.g., p1.z = 5;), its shape changes. Any function optimized for the old shape will deoptimize when called with the new object .
Polymorphism/Megamorphism: If a function call site that was previously 'monomorphic' (always seeing the same object shape) starts receiving objects of different shapes, it becomes 'polymorphic'. If this happens too often ('megamorphic'), the engine may deoptimize because the inline cache can't efficiently handle the variety .
Array Element Kind Changes: V8 tracks the type of elements in an array (e.g., 'PACKED_SMI_ELEMENTS' for small integers). Adding a different type, like a float or a string, changes the array's 'ElementsKind' and can trigger deoptimization of code optimized for the previous kind .
Function Invalidation: Deoptimization can happen because a dependency is broken. For instance, if function f1 is optimized, and then another function change_o modifies an object in a way that invalidates a key assumption f1's optimized code depended on, f1 will be marked for deoptimization .
Deoptimizations can be 'eager' or 'lazy'. An eager deopt happens immediately for the currently executing function when an assumption fails, such as at a type check in the code . A 'lazy deoptimization' is different: it marks an optimized function (which may have other calls on the stack) as invalid, but doesn't interrupt it right away. Instead, it rewrites the function's return addresses so that the deoptimization occurs lazily when control returns to that function, making the process more efficient .
Performance Penalty: Deoptimization is costly. It involves discarding optimized machine code, reconstructing the interpreter state, and potentially re-collecting type feedback . This can lead to noticeable slowdowns, especially in hot code paths .
Re-optimization: After a deopt, execution continues in the interpreter. If the function becomes hot again, the engine may attempt to re-optimize it, potentially with a new set of assumptions based on the new feedback . A function can be optimized and deoptimized many times.
Cascading Effects: A single deoptimization can have a ripple effect. As shown in a real-world bug report, deopts can cause a function's runtime to double (e.g., from 120ms to 250ms), and accumulated deopts can drastically increase application warmup time (e.g., from 14 seconds to 55 seconds) .
Understanding deoptimization helps in writing high-performance JavaScript. By writing 'monomorphic' code that consistently uses the same object shapes and variable types, developers can help the engine's inline caches stay in their fastest state and avoid the performance cliffs that deopts create .