Monomorphic vs Polymorphic JavaScript Performance

Monomorphic and polymorphic function dispatches represent how modern JavaScript engines handle operations and property accesses based on the structural shapes of objects. Because JavaScript is dynamically typed, just-in-time (JIT) compilers use mechanisms like hidden classes and inline caching to optimize function calls. A monomorphic call site, which repeatedly encounters the exact same object shape, allows the engine to generate optimized, direct machine code, whereas polymorphic and megamorphic call sites introduce branch checks and fallback lookups that significantly degrade execution speed.

Hidden Classes and Inline Caching

To understand dispatch performance, you must understand how JavaScript engines like V8, SpiderMonkey, and JavaScriptCore optimize property lookups:

The Dispatch States

Inline caches operate in three primary states, transitioning from fastest to slowest:

1. Monomorphic Dispatch

A call site is monomorphic when it only ever encounters objects of a single shape.

function getX(point) {
    return point.x;
}

// Monomorphic: Every object passed has the same shape {x, y}
getX({ x: 1, y: 2 });
getX({ x: 3, y: 4 });

2. Polymorphic Dispatch

A call site becomes polymorphic when it encounters a small number of distinct shapes—typically between two and four.

// Polymorphic: The function handles two different shapes {x, y} and {x, y, z}
getX({ x: 1, y: 2 });
getX({ x: 3, y: 4, z: 5 });

3. Megamorphic Dispatch

When a call site encounters more than four or five different shapes, the inline cache overflows into a megamorphic state.

// Megamorphic: Passing many distinct object shapes to the same call site
getX({ x: 1, a: 1 });
getX({ x: 2, b: 2 });
getX({ x: 3, c: 3 });
getX({ x: 4, d: 4 });
getX({ x: 5, e: 5 });

Performance Comparison

Dispatch Type Shapes Handled Lookup Strategy JIT Inlining Viability Relative Speed
Monomorphic 1 Direct memory offset Highly Likely Fastest
Polymorphic 2 – 4 Linear branch checks Limited Moderate
Megamorphic 5+ Global cache / Hash table None Slowest

In high-performance scenarios, monomorphic dispatch can be multiple times faster than polymorphic dispatch and orders of magnitude faster than megamorphic lookups, primarily because monomorphic code enables aggressive function inlining and dead-code elimination.

How to Keep Code Monomorphic

  1. Initialize Properties Consistently: Always initialize properties in the exact same order within constructors or factory functions to ensure they share the same internal shape.
  2. Avoid Deleting Properties: Using delete obj.prop mutates the shape into a generic dictionary mode, immediately hurting cache efficiency. Assign null or undefined instead.
  3. Keep Function Signatures Homogeneous: Avoid passing numbers, strings, and custom objects interchangeably to the same utility function in performance-critical hot paths.