Performance of JavaScript optional chaining

November 08, 2019 0 Comments

Performance of JavaScript optional chaining

 

 

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

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 migrate to the new syntax in such a case,
you’ll just add 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 many.

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?

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’s 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 <br />
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 bothering about.

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, or 25 M is enough? Could the slower implementation decrease FPS below 60? Does it
make sense to fight against these few kilobytes coming from transpilation? Is it possible that the loading time of the app
increases significantly because of them?

You have all the data now, you can decide.



Tag cloud