Performance of optional chaining

Eryk Napierała - Nov 7 '19 - - Dev Community

One of the coolest features added in just announced TypeScript 3.7 is optional chaining syntax. It promises a much shorter and more readable code for dealing with deeply nested data structures. How may this nice new feature affect the performance of your project?

At first sight, optional chaining syntax can make the codebase significantly smaller. Instead of writing monstrous code like this one:

foo && foo.bar && foo.bar.baz && foo.bar.baz.qux

you can write this

foo?.bar?.baz?.qux;

19 characters instead of 48. Quite concise!

Bundle size

The thing is, it's very unlikely that you'll ship the new syntax to the end-user. At the time of writing the post, the only browser supporting it is Chrome 80. So, at least for now the transpilation is must-have.

How does the expression above look in plain old JavaScript?

var _a, _b, _c;
(_c = (_b = (_a = foo) === null || _a === void 0 ? void 0 : _a.bar) === null || _b === void 0 ? void 0 : _b.baz) === null || _c === void 0 ? void 0 : _c.qux;

That's, well, far more than 19 characters, even more than 48 you could have before. To be precise, it's 172 characters! Minification decreases this number, but it's still 128 - 6 times more when compared with the source code.

var _a,_b,_c;null===(_c=null===(_b=null===(_a=foo)||void 0===_a?void 0:_a.bar)||void 0===_b?void 0:_b.baz)||void 0===_c||_c.qux;

Fortunately, the TypeScript compiler isn't the only option we have. Babel provides support for optional chaining as well.

Let's check how it deals with the new syntax. Is it any better than TypeScript? It doesn't look like! 244 characters.

var _foo, _foo$bar, _foo$bar$baz;

(_foo = foo) === null || _foo === void 0 ? void 0 : (_foo$bar = _foo.bar) === null || _foo$bar === void 0 ? void 0 : (_foo$bar$baz = _foo$bar.baz) === null || _foo$bar$baz === void 0 ? void 0 : _foo$bar$baz.qux;

However, after running Terser on the code, the code is smaller than minified TypeScript output - 82 characters.

var l,n;null==u||null===(l=u.bar)||void 0===l||null===(n=l.baz)||void 0===n||n.qux

So in the best scenario, we're getting around 4 characters in the final bundle for each one of the source code. How many times could you use optional chaining in a medium-sized project? 100 times? If you'd migrate to the new syntax in such a case, you've just added 3,5 kB to the final bundle. That sucks.

Alternatives

Let's take a step back. Optional chaining isn't a new idea at all. Solutions for the incredibly && long && double && ampersands && chains problem have already existed in the so-called userspace for quite some time. Jason Miller's dlv is only one among the many.

dlv(foo, 'bar.baz.qux');

Besides, this approach isn't as good as the new syntax, because it's not type-safe, it requires slightly more code on the call site - 25 characters. Plus, you must import the function from the library. But, how does the code look in the final bundle?

d(u,'bar.baz.qux');

What a surprise! 19 characters, that's as concise as optional chaining syntax itself.

If you feel uncomfortable with strings, you can pass an array of strings to the function. Although there are more characters in both source and the final code, it may be worth doing. You will see later why.

dlv(foo, ['bar', 'baz', 'qux']);

Implementation of the function itself takes only 101 characters after minification.

function d(n,t,o,i,l){for(t=t.split?t.split("."):t,i=0;i<t.length;i++)n=n?n[t[i]]:l;return n===l?o:n}

It means it's enough to use optional chaining transpiled with Babel twice and you'll get more code than with dlv. So, is the new syntax no-go?

Parsing time

The amount of the code affects not only downloading a file but also the time of parsing it. With estimo, we can estimate (😉) that value. Here are the median results of running the tool around 1000 times for all variants, each containing 100 equal optional chainings.

code parsing time

It seems that parsing time depends not only on the size of the code but also on the syntax used. Relatively big "old spice" variant gets significantly lower time than all the rest, even the smallest one (native optional chaining).

But that's only a curiosity. As you can see, at this scale differences are negligible. All variants are parsed in time below 2 ms. It happens at most once per page load, so in practice that's a free operation. If your project contains much more optional chaining occurrences, like ten thousand, or you run the code on very slow devices - it might matter. Otherwise, well, it's probably not worth to bother.

Runtime performance

Performance is not only about the bundle size, though! How fast is optional chaining when it goes to execution? The answer is: it's incredibly fast. Using the new syntax, even transpiled to ES5 code, may give 30x (!) speedup comparing to dlv. If you use an array instead of a string, though, it's only 6x.

jsPerf results

No matter whether you access empty object, full one or one with null inside, approaches not employing accessor function are far more performant.

Conclusion

So, is optional chaining fast or slow? The answer is clear and not surprising: it depends. Do you need 150 M operations per second in your app? Don't choose a library. Is 25 M enough but you count each byte of your bundle size? You may consider something like dlv. Want to have both? Ship native version to the latest browsers and use Babel to provide fallback for older ones.


This post Performance of JavaScript optional chaining appeared first on Allegro.tech Blog.

. . . . . . . . . . .