How V8 Thinks About Your JavaScript

JavaScript's dynamic typing means the engine can't rely on static type information the way a C++ compiler can. V8 works around this by constructing hidden classes for objects at runtime. Objects that share a hidden class can reuse the same optimized machine code. When two object instances diverge in shape — say, one gets an extra property added later — their hidden classes split and V8 can no longer share that generated code.

The practical rules follow directly: initialize all object members in constructor functions, and always initialize them in the same order. That keeps instances of the same constructor on one hidden class.

Numbers and Arrays

V8 uses tagging to represent values whose types may shift at runtime, and it infers the numeric type from the values you actually use. Consistency matters: changing the tag on a value has a cost. Where possible, stick to 31-bit signed integers, which are the most efficient representation V8 supports.

Array storage comes in two internal flavors: fast elements for compact, contiguous key sets, and dictionary elements for sparse arrays. Flipping between the two is expensive, so avoid it.

  • Use contiguous keys starting at 0.
  • Don't pre-allocate arrays larger than roughly 64K elements to full size; grow them incrementally instead.
  • Don't delete elements, especially from numeric arrays.
  • Don't read from uninitialized or deleted slots.

Arrays of doubles get special treatment: they're unboxed internally, which means faster access, but also means the array's hidden class shifts when element types change. For small, fixed-size arrays, an array literal lets the compiler determine the hidden class up front and avoid repeated boxing and unboxing conversions:

Assigning element-by-element can cause the array to convert to an unboxed double array, then back again, depending on what you assign next. A literal avoids that churn. For small arrays under 64K, preallocating to the final size before use is also fine. Don't mix objects into numeric arrays, and if you initialize without a literal, be aware of the re-conversion cost.

The Two Compilers

V8 pairs a full compiler that produces decent code quickly and runs on everything, with an optimizing compiler that waits for hot functions and produces far better code. The full compiler assumes nothing about types; it uses inline caches (ICs) to learn about types as the code executes. Those ICs make type validation cheap for the common case, but the benefit erodes as an operation accepts more types.

That's why monomorphic code — where the hidden classes of all arguments are the same on every call — wins. One call that passes a different type flips an operation to polymorphic and it stops being a candidate for efficient inline caching.

The optimizing compiler recompiles functions that run often, using the type feedback collected by the ICs. Hot, monomorphic functions can be inlined wholesale. But the optimizer bails out on functions that contain try {} catch {} blocks. If you have exception handling around performance-critical work, keep that work in a nested function and wrap only the call site in the try/catch.

You can observe what the optimizer does with the standalone V8 shell d8. The --trace-opt flag logs which functions get optimized or bailed out, and optimizing compiler logs are available at runtime:

d8 --trace-opt my-app.js

Deoptimization

The optimizing compiler's work is speculative, and when its assumptions break it deoptimizes back to full-compiler code. That means discarding optimized code with a performance hit, at least until reoptimization kicks in again. Changing an object's hidden class after an optimized function is running is the most common trigger. So once a function is optimized, avoid hidden class changes on the objects it touches.

The --trace-deopt flag on d8 logs deoptimizations as they happen:

d8 --trace-deopt my-app.js

Chrome also accepts V8 tracing options at startup, so you can pass the same flags to the browser binary. Profiling through d8 works too — it has a built-in sampler that writes a v8.log file:

d8 --prof my-app.js

Where to Spend Your Time

Before any of this matters, confirm the problem is actually in your JavaScript. Run PageSpeed and rule out DOM or network issues first. When you do narrow things down to JavaScript, understand how V8's hidden classes, inline caches, and optimizing compiler interact with your code so you fix the right thing. The steps stay the same: prepare, isolate the real bottleneck, then fix what matters.