Cost of unnecessary Optional Chaining (and Nullish coalescing operator)
TusharShahi
Posted on April 6, 2024
If you are a developer who has worked with JavaScript in recent years, I am sure you are well aware of the below syntax:
const a = obj1?.x;
const b = obj2.c ?? '2'
We see them all around us, making our lives easier.
The first one is optional chaining and the other one is the nullish coalescing operator.
Both of them are so easy to use that they have a huge adoption in a lot of code bases. They help protect us from the embarrassing situation of our webapp breaking in production. But there is a cost to their overuse which will make one debate their pros and cons.
Background
As we can see from the docs, both are recent additions to the language. This means to get them to work on older browsers, they need polyfills.
We can open our dev tools and write something like window?.testing
and it will not complain. But if we do the same on an older browser, it will throw us an error.
For our nice new syntax to work on old outdated browsers, it is converted into (probably) longer and more primitive code. This whole logic is what a polyfill is. It is a script that updates/adds new functions.
Babel
So, how is this working out in the case of our React apps. We never have to worry about writing some polyfill. The reason for that is Babel
. Babel, a transpiler, converts modern JavaScipt code to make it easily understandable to most browsers. We get to decide the target browsers we plan on supporting and Babel does the conversion.
Your code might not use Babel but some other transpiler like SWC, but the funda is common. Convert new-fashioned code into code that most browsers can understand.
All this is part of the bundling process, with the final aim of converting our React code into a group of JS, CSS and HTML files.
Being extra cautious
Coming to the topic that encouraged me to write this blog. I was reviewing a code and saw something like below:
if (cityName) {
if (location === cityName)
res.writeHead(301, {
Location: `/somegibberish/${cityName?.toLowerCase()}/`,
});
else
res.writeHead(301, {
Location: `/somegibberish/${cityName?.toLowerCase()}/${localitySlug}`,
});
}
I pointed out the problem: cityName
is checked once at the top level, and inside the if
branch, optional chaining is used to check the (somewhat) same thing.
The response: Although unnecessary, there is no harm in doing that.
The first harm I could see was in setting the wrong expectations. If the above code block became much larger with multiple contexts like function calls, it would be hard to track whether cityName
can be falsy
. (This is a problem, more specific to JS-only code bases)
The second is something I found out only when looking at the bundle output.
Transpiling
The code written above will not go as it is into our bundle. As already established, it will get converted into old-school JS so we can target as many browsers (in turn as many users) as possible.
Here is the screenshot of what optional chaining and nullish coalescing operator code gets converted into.
Let us pick any one of them.
Nullish Coalescing Operator: a logical operator that returns its right-hand side operand when its left-hand side operand is null
or undefined
, and otherwise returns its left-hand side operand.
Something like x ?? '2'
gets converted into x !== null && x !== void 0 ? x : '2'
.
void 0
is not something you see every day. It is the old way of writing undefined
. It is favored over directly writing undefined
for 2 reasons:
In older JS specifications,
undefined
was not a reserved word. One could literally dovar undefined = 'defined'
. (This is no longer a problem)undefined
takes more bits thanvoid 0
. Which definitely is a good enough reason to use it, when our bundle will be filled with it.
Cost to the bundle
Here is how the above code snippet, sat in our code bundle:
The above gives a sense of the problem with using optional chaining and NCO (in short :P) blindly, even more so in situations where they are not required. The bundle size can increase massively, even after modification.
I would still use the above two features, but I strongly advise not to use them unnecessarily now that I have seen the bundle output.
Bonus: Here is an es-lint rule that might help: no-unnecessary-condition
in pruning out all those unnecessary checks.
Posted on April 6, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.