Understanding a Performance Issue with “Polymorphic” JSON Data

How objects with the same shape but different kinds of values can have a surprising effect on JavaScript performance

Jan Pöschko
Feb 28, 2019 · 8 min read

While working on some low-level performance optimizations for rendering of Wolfram Cloud notebooks, I noticed a rather strange issue where a function entered a slower execution path due to dealing with floating-point numbers, even though all data ever passed into it were integers. Specifically, cell counters were treated as floating-point numbers by the JavaScript engine, which slowed down rendering of large notebooks quite a bit (at least in Chrome).

We represent cell counters (defined by CounterAssignments and CounterIncrements) as an array of integers, with a separate mapping from counter names to offsets in the array. This is a bit more efficient than storing a dictionary for each set of counters. E.g. instead of

we store an array

and keep a separate (global) mapping

from names to indices. As we render a notebook, each cell keeps its own copy of the current counter values, performs its own assignments and increments (if any), and passes on a new array to the next cell.

I found out that — at least sometimes — V8 (the JS engine in Chrome and Node.js) treated the numeric arrays as if they contained floating-point numbers. This slows down many operations on them, since the memory layout for floats is not as efficient as for (small) integers. This felt strange since the arrays never contained anything but such Smis (integers in the signed 31-bit range, i.e. from -2³⁰ to 2³⁰-1).

I found a workaround, “forcing” all values to turn into integers by applying before putting them into the counter arrays, after reading them from some JSON object — even though they already were integers in the JSON data anyway. While I had a workaround, I didn’t fully understand why it was actually working — until recently…

The explanation

The talk JavaScript engine fundamentals: the good, the bad, and the ugly by Mathias Bynens and Benedikt Meurer at AgentConf finally cleared things up for me: It’s all about the internal representation of objects in the JS engine, and how each object is linked to a certain shape.

The JS engine keeps track of the property names defined on an object, and whenever a property is added or removed, a different shape is used in the background. Objects of the same shape keep the same property at the same offset in memory (relative to the object’s address), allowing the engine to speed up property access significantly and reducing the memory boilerplate of individual object instances (they don’t have to maintain a whole dictionary themselves).

What I hadn’t known before was that the shape also distinguishes between different kinds of property values. Particularly, a property with small-integer values implies a different shape than a property that (sometimes) contains other numeric values. E.g. in

a shape transition happens with the second assignment, from a shape where property is known to have a Smi value to a shape where property can be any Double. The previous shape is then “deprecated” and not used anymore moving forward. Even other objects that never actually used a non-Smi value will switch to the new shape as soon as their property is used in any way. This slide sums it up very well.

So this is exactly what happened in our case with counters: The definitions of and came from JSON data such as

but we also had values like

for other parts of a notebook. Even though no objects were used for counters, the mere existence of such objects caused all objects to also change their shape. Copying their into the counter arrays then caused those arrays to switch to a less efficient representation as well.

Inspecting internal types in Node.js

We can inspect what’s going on under the hood in V8 by using natives syntax. This is enabled with the command-line argument . The full list of special functions is not officially documented but there is an unofficial list. There is also a package v8-natives for slightly more convenient access.

In our case, we can use to determine whether a given array has Smi elements:

Running this program gives the following output:

After constructing an object with the same shape but a floating-point , using the original object (containing an integer value) again yields a non-Smi array.

Measuring the impact on a standalone example

To illustrate the effect on performance, let’s use the following JS program ():

We construct an array of 100 integers extracted from an object , and then we call 10 million times, which creates a copy of the array, changes one item in the copy, and returns the new array. This is essentially what happens when cell counters are processed while rendering a (large) notebook.

Let’s change the program a bit and add the following code in the beginning ():

The mere existence of this object will cause the other to change its shape and slow down operations on the arrays constructed from its .

Note that creating an empty object and adding a property later has the same effect as parsing a JSON string:

Now compare the execution of these two programs:

This is with Node v11.9.0 (running V8 version But let’s try all major JS engines:

Image for post
Image for post

V8 is used in Chrome, SpiderMonkey in Firefox, Chakra in IE and Edge, JavaScriptCore in Safari.

Measuring execution time of the whole process isn’t ideal, but we can mitigate outliers by focusing on the median of 100 runs per example (in randomized order, sleeping 1 second between runs) using multitime:

A few things to note here:

  • Only in V8, there’s a significant difference (about 0.08s or 10%) between the two approaches.
  • V8 is faster than all other engines, for both the Smi and the float approach.
  • Standalone V8 as used here was significantly faster than Node 11.9 (which uses an older version of V8). I guess this is mostly due to general performance improvements in more recent V8 versions (notice how the difference between Smi and float approach was reduced from 0.35s to 0.08s), but some overhead of Node compared to V8 might also be at play.

You can look at the full source of the test file. All tests were performed on a late-2013, 15" MacBook Pro running macOS 10.14.3 with a 2.6 GHz i7 CPU.


Shape transitions in V8 can have some surprising performance consequences. Usually, you don’t have to worry about this in practice (especially since V8, even on its “slow” path, might still be faster than other engines). But in a highly performance-critical application, it’s good to keep in mind the effects of a “global” shape table, where distant parts of an application can affect one another.

If you’re dealing with external JSON data not under your control, you can “convert” a value to an integer using bitwise OR as in , which will also make sure its internal representation is a Smi.

If you have control over the JSON data, it might be a good idea to only use the same property names for properties with the same underlying value type. E.g. in our case it might be better to use

instead of calling the property in both cases. In other words: Avoid “polymorphic” objects.

Even if the effects on performance are neglible in practice, it’s always interesting to get a better understanding of what’s going on under the hood, in this case of V8. For me personally, it was a great aha moment when I found out why an optimization I made a year ago actually worked.

For further information, here are links to various talks again:

Wolfram Developers

Engineering Blog by the Makers of Mathematica, Wolfram…

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch

Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore

Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store