Surprising polymorphism in React applications

Modern web applications based on the React framework usually manage their state via immutable data structures, i.e. using the popular Redux state container. This pattern has a couple of benefits and is becoming ever more popular even outside the React/Redux world.

The core of this mechanism are so-called reducers. Those are functions that map one state of the application to the next one according to a specific action — i.e. in response to user interaction. Using this core abstraction, complex state and reducers can be composed of simpler ones, which makes it easy to unit test the code in separation. Consider the following example from the Redux documentation:

The todo reducer maps an existing state to a new state in response to a given action. The state is represented as plain old JavaScript object. Looking at this code from a performance perspective, it seems to follow the principles for monomorphic code, i.e. keeping the object shape the same.

Speaking naively the property accesses in render should be monomorphic, i.e. the state objects should have the same shape — map or hidden class in V8 speak — all the time, both s1 and s2 have id, text and completed properties in this order. However, running this code in the d8 shell and tracing the ICs (inline caches), we observe that render sees different object shapes and the state.id and state.text property accesses become polymorphic:

So where does this polymorphism comes from? It’s actually pretty subtle and has to do with the way V8 handles object literals. Each object literal — i.e. expression of the form {a:va,...,z:vb} defines a root map in the transition tree of maps (remember map is V8 speak for object shape). So if you use an empty object literal {} than the transition tree root is a map that doesn’t contain any properties, whereas if you use {id:id, text:text, completed:completed} object literal, then the transition tree root is a map that contains these three properties. Let’s look at a simplified example:

You can run this code in Node.js passing the --allow-natives-syntax command line flag (which enables the use of the %HaveSameMap intrinsic), i.e.:

So despite these objects a and b looking the same — having the same properties with the same types in the same order — they don’t have the same map. The reason being that they have different transition trees, as illustrated by the following diagram:

So polymorphism hides where objects are allocated via different (incompatible) object literals. This especially applies to common uses of Object.assign, for example

still yields different maps, because the object b starts out as empty object (the {} literal) and Object.assign just slaps properties on it.

This also applies if you use spread properties and transpile it using Babel, because Babel — and probably other transpilers as well — use Object.assign for spread properties.

One way to avoid this is to consistently use Object.assign so that all objects start from the empty object literal. But this can become a performance bottleneck in the state management logic:

That being said, it’s not the end of the world if some code becomes polymorphic. Staying monomorphic might not matter at all for most of your code. You should measure really carefully before making the decision to optimize the wrong thing.

Show your support

Clapping shows how much you appreciated Benedikt Meurer’s story.