Transpiled ES6 code can get sizable, which doesn’t benefit limited bandwidth users, such as those using your mobile website. Every byte matters, especially as web applications grow in size.
With a special emphasis on React JS application development, I’ll show a few approaches to writing performant code that also results in a smaller footprint when it comes out of your build process.
It goes without saying that UglifyJS or a similar minification plugin is an absolute must-have for transpiled code. Developing code with minification in mind is very important. You should know how variables will be changed or how to write clean code that will be properly reduced in size in production build.
Update: The original article compared minified code. Per requests from the community members, I updated the results to include the compressed sizes as well. Compression changed the perspective for some of my original suggestions, while others are amplified.
Recreating Objects for Immutability
Redux users, in particular, are used to recreating objects to advertise a change in state.
let newState = Object.assign({}, state); // 38 bytes minified // 68 bytes gzipped
Object.assign
is wonderful and comes with ES2015. Object spread is an alternative that is still a proposal for future versions of ECMAscript, but it’s readily available for use via Babel (and other transpilers).
let newState = { ...state }; // 33 bytes minified (13% improvement) // 63 bytes gzipped (7% improvement)
Object spread operator made our code even easier to read and a tiny bit smaller. Chances are you will use spread a lot, whether in Redux, JSX properties, or generally within you application so it will add up.
It’s important to know that Babel will create a small _extends
function, which is basically a polyfill for Object.assign
. This adds 175 bytes to your codebase (uncompressed), but is used for functionalities other than object spread.
Our first example was easy and yielded a solid improvement in the minified code size. Next we’ll see how to squeeze out even more juice in common operations with JSX.
Conditional Statements in React Components
Conditional rendering is a frequent task in many applications, whether we submit properties conditionally or choose proper component for the task.
Conditional Properties
When your React components require properties based on conditional criteria, you should calculate those props before writing JSX. This simplified example illustrates that:
let cmp = condition === true ? <div foo={1}/> : <div foo={2} />; // 85 bytes minified // 112 bytes gzipped
It looks like a short and sweet one-liner. Let’s change it a bit:
let bar = condition === true ? 1 : 2; let cmp = <div foo={bar} />; // 60 bytes minified (30% improvement) // 104 bytes gzipped (7% improvement)
So why do you think the bottom code results in 30% less code? The key is visualizing the transpiled code. The JSX will become a call to React.createElement
function. That leads to a lot of code duplication, which we generally want to avoid.
The second version has a single JSX block and manages variables to passed to React.createElement as arguments. The transpiled code would look something like:
var cmp = condition === true ? React.createElement('div', { foo: 1 }) : React.createElement('div', { foo: 2 }); // vs var bar = condition === true ? 1 : 2; var cmp = React.createElement('div', { foo: bar });
Thankfully, gzip makes the difference less significant.
Conditional Components
A common use case for conditional components is choosing the right component to serve as a wrapper. For example, if the data object contains a URL then wrap in an <a> anchor. Otherwise use a <div> layer.
I’ll work with abstract components to showcase this logic:
function xyz() { if (condition === true) { return <Foo><UniversalChild /></Foo> } return <Bar><UniversalChild /></Bar> } // 171 bytes minified // 128 bytes gzipped
All we care for doing here is wrapping UniversalChild
into a proper container. Imagine this child being a whole block of JSX. It’s ugly, hard to maintain, and unbelievably common.
In case you wanted to nest a number of components using this approach, then I definitely recommend wrapping them in to a stateless component.
The next example trims this down by 40%, minified:
function xyz() { return React.createElement(condition === true ? Foo : Bar, null, <UniversalChild />) } // 104 bytes minified (40% improvement) // 121 bytes gzipped (6% improvement)
Yes, it’s a weird combination of React.createElement
and JSX, but it gets the job done.
The caveat of this approach is that it’s bearable only if you have a single child to wrap. A whole block would be much uglier or would involve over-nesting. While we were able to slightly reduce size, I would personally avoid using this line as the code is definitely harder to read.
A more human-friendly alternative can yield smaller and a very readable original code.
function yxz() { let RootCmp = condition === true ? Foo : Bar; return <RootCmp><UniversalChild /></RootCmp> } // 112 bytes minified (35% improvement) // 135 bytes gzipped (5% more bytes)
Here we create a new reference and point it to the appropriate component. Remember that JSX will become a function call so references are very much allowed.
The paradox here is that minified code is significantly better (35%) than the original example, but when gzipped it gets slightly larger (5%).
I like this approach because it lets me nest a much larger JSX block for children while tackling the wrapper in a single line. Less repetition.
Conditional JSX operations are common and you will likely reuse this in many areas of your app. Another frequent dilemma is how to optimally nest repeating components.
Optimized Nesting of Repeating Components
Ever created A List, Grid, or a Menu with repeating items? Something like this:
let cascadingMenu = ( <Menu> <MenuItem>One</MenuItem> <MenuItem>Two</MenuItem> <MenuItem>Three</MenuItem> <MenuItem>Four</MenuItem> <MenuItem>Five</MenuItem> </Menu> ); // 258 bytes minified // 118 bytes gzipped
If you read the previous section, you’ll know that this block results in a number of function calls. It certainly looks readable (especially with syntax highlighting), but can we do better?
let items = ['One', 'Two', 'Three', 'Four', 'Five']; let cascadingMenu = ( <Menu>{items.map((item, key) => <MenuItem key={key}>{item}</MenuItem>)}</Menu> ) // 162 bytes minified (37% improvement) // 160 bytes gzipped (36% more bytes)
This example shows the full power of gzip. Even though the minified version is much slimmer, gzip can’t compress as much as it could in the original example.
I personally like embedding this functionality into the top level component — in this case Menu.
let items = ['One', 'Two', 'Three', 'Four', 'Five']; let cascadingMenu = <Menu items={items} />; // 100 bytes minified (61% improvement) // 118 bytes gzipped (no change)
We finally got a cleaner source at a cost of the excess bytes when we implement the loop inside Menu
.
When I originally compared minified results, the improvements were reasonable and clear. However, compression changes everything, allowing developers to write readable, yet performing code.
Arrow functions
We all love arrow functions. But there’s a use case for them. Don’t replace regular functions with arrow functions just because it’s the cool new thing.
In this example I’ll create a noop
function. These are useful as default callbacks or placeholders so we could easily reuse throughout an app.
function noop() {}; // 17 bytes minified // 17 bytes gzipped
Arrow function counterpart could look like this:
let noop = () => {}; // 22 bytes minified (29% more bytes) // 27 bytes gzipped (59% more bytes)
Compression made the difference even more staggering, relative to the plain old function above.
There are a number of reasons why I would not recommend using arrow functions for this purpose.
- It’s not easier to read
- Produces more code
- It’s also a trap. Is {} going to return an empty object or undefined?
Answer: It will return undefined. let noop = () => ({});
returns an empty object. It’s an exception on the ugly side of JavaScript. See more here.
Arrow functions are phenomenal, don’t get me wrong. They are just not meant to be used as a complete replacement for the regular function statement.
Destructuring With Care
I’m a fan of destructuring in JavaScript. But when I saw what it does to the transpiled code, I became a lot more careful about what I use it for.
Many developers will dislike repeating this in a function. It can’t be minified and it can get misleading when switching context a lot.
function context() { this.a(); this.b(); this.c(); this.d(); } // 56 bytes minified // 55 bytes gzipped
To make it prettier, many ES6 developers will turn to destructuring. In fact, destructuring destroyed it. Say that out loud, quickly, 10 times.
function context() { let { a, b, c, d } = this; a(); b(); c(); d(); } // 76 bytes minified (36% more bytes) // 87 bytes gzipped (58% more bytes)
A more traditional way to deal with repeating this keyword is referencing it.
function context() { let me = this; me.a(); me.b(); me.c(); me.d(); } // 55 bytes minified (2% improvement) // 59 bytes gzipped (7% more bytes)
For the longest time I thought caching this
would add value. And it does in minified code. However, gzip is smart enough to handle repeating keywords. Caching this
for the sole purpose of reducing code size doesn’t seem viable.
I can’t say I recommend using any one of these approaches. But do know how they work and when you can benefit from each. Finally, let’s see how default arguments work.
Default Arguments
Default argument values are absolutely great for code readability. However, they come at a cost.
function default1(foo = 'bar') {} // 82 bytes minified // 93 bytes gzipped
Wait, what? 82 bytes? Well take a look at the code produced:
function default1() { var foo = arguments.length <= 0 || arguments[0] === undefined ? 'bar' : arguments[0]; }
Most of the code Babel returned cannot be minified, which is why it’s so huge. And if we added another argument with default value, the code gets duplicated.
If we were to care about the final size of our JS bundle, we might consider this alternative:
function default2(foo) { if (foo === undefined) { foo = 'bar'; } } // 43 bytes minified (48% improvement) // 54 bytes gzipped (58% improvement)
The improvement is huge — a staggering 58%! However, it comes at the expense of cluttered code. Especially if several arguments with default values are needed.
Performance vs Maintainable Code
ES2015 is much about syntactic sugar. This is a great thing because it makes it much easier to write and maintain great code. The tools we have available make sure development experience doesn’t come as a significant expense in application size.
The examples above show how to think of the transpiled code when writing ES6 apps. Obviously minification and compression are not the same thing, and some of the findings came as a surprise, at least to the author.
So where is the line between performance and maintainable code? This is a crucial decision for a software architect to make, and there’s no rule of thumb. Customer facing apps will likely have to be as optimized as possible, whereas internal, enterprise products could allow for better code quality. Large apps would benefit from more maintainable code, but framework-grade projects will want to squeeze out any performance.
In the long run, all of these benchmarks fall into the category of microoptimization. Don’t fix your code if your app suffers for other architectural issues, unoptimized assets, etc. Spend 20% of your time to fix 80% of performance bottlenecks first, and then you can observe the results I presented here.
Also the findings I presented here will likely change in the future. Browsers evolve and so do transpilers (in this case Babel).
Please take this data with you, experiment, and share your findings. I would appreciate it if you liked this post and shared with your network.
Update: Thanks to your suggestions, I added values after compression. They made the benefits of some of my recommendations dissolve due to the gzip awesomeness. This only proved that the tools we have available are here to make our life easier. Writing maintainable code is the best performance improvement.
**This article was originally published on May 16, 2016.
Grgur Grisogono
Related Posts
-
ES5 in Production, ES6 in Development
Many modern browsers already ship with ES2015 support, especially our favorite development browsers such as…
-
ES6 Import Statement Without Relative Paths Using Webpack
The ES2015 module system is probably familiar to you by now. If you’ve used it,…