A monomorphic function is one that consistently receives objects of the same shape (Hidden Class), while a polymorphic function receives objects of different shapes, causing the engine's inline caches to handle multiple type profiles .
In JavaScript engine optimization, 'monomorphic' and 'polymorphic' refer to the state of a function's inline cache (IC) at a specific property access site . When a function accesses a property (like obj.x), the engine caches information about the object's shape (Hidden Class) and the property's offset. Over time, based on the variety of shapes seen, this cache transitions through different states. Identifying these states helps developers understand optimization potential and performance bottlenecks .
Uninitialized: The initial state when a property access site hasn't been executed yet.
Monomorphic: The site has seen exactly one object shape. This is the most optimized state, as the engine can directly access the property using the cached offset without any shape checking overhead .
Polymorphic: The site has seen a small number (typically 2-4) of different shapes. The engine maintains a small cache of shapes and offsets, requiring a shape check for each possibility. This is less efficient than monomorphic but still manageable .
Megamorphic: The site has seen many different shapes (typically 5+). The engine gives up on caching and falls back to a full dictionary lookup, which is significantly slower .
The critical insight is that object shape is determined not just by which properties exist, but also by their creation order . Two objects with the same properties created in different orders will have different Hidden Classes, leading to polymorphism even though they appear identical to the developer .
So how do you actually identify monomorphic vs. polymorphic functions in practice? Modern JavaScript engines provide tools for this analysis:
V8's --trace-ic Flag: Running Node.js with the --trace-ic flag logs inline cache information. It shows each property access site and its cache state (monomorphic, polymorphic, megamorphic) along with the shapes seen . This is the most direct way to see what's happening .
V8's --trace-maps: This flag traces Hidden Class (Map) transitions, helping you understand why objects are getting different shapes and causing polymorphism .
Chrome DevTools Performance Tab: Recording a performance profile and looking at the 'Summary' or 'Bottom-Up' views can show functions that are deoptimizing frequently. Polymorphic functions are a common cause of deoptimizations .
v8-deopt-viewer: Tools like the v8-deopt-viewer can parse deoptimization logs and help identify functions that are being deoptimized due to type instability or shape changes .
Benchmarking with Different Inputs: A practical approach is to benchmark a function with monomorphic inputs (same shape) vs. polymorphic inputs (different shapes). Significant performance differences often indicate optimization issues .
Understanding monomorphic vs. polymorphic code is crucial for writing high-performance JavaScript. Monomorphic code stays in the fastest inline cache state, allowing the engine to generate highly-optimized machine code with minimal type checks. Polymorphic code incurs additional overhead from shape checks and may prevent or limit function inlining. Megamorphic code effectively disables these optimizations, leading to significantly slower execution . For critical hot paths, ensuring objects consistently have the same shape can yield substantial performance improvements .