Sign in
Log inSign up
Optimize ES6 Output Size & Performance

Optimize ES6 Output Size & Performance

Grgur Grisogono's photo
Grgur Grisogono
·May 16, 2016

Transpiled ES6 code can get sizeable, which doesn't benefit limited bandwidth users, such as those using your mobile web site. Every byte matters, especially as web applications grow in size.

With a special emphasis on React JS application development, I'll to show a few approaches to writing performant code that also results in a smaller footprint when it comes out of your build process.

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.

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

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)

Object spread operator made our code even easier to read and a bit smaller. Chances are you will use spread a lot, whether in Redux, JSX properties, or generaly 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, 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

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)

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 });

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

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%:

function xyz() {
  return React.createElement(condition === true ? Foo : Bar, null, <UniversalChild />)
}
// 104 bytes minified (40% 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.

A much prettier block of code will result in a bit larger footprint, but still 35% better than the original example. It's also very human-friendly.

function yxz() {
  let RootCmp = condition === true ? Foo : Bar;
  return <RootCmp><UniversalChild /></RootCmp>
}
// 112 bytes minified (35% improvement)

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.

I like this approach because it also let's me nest a much larger JSX block for children while tackling the wrapper in a single line.

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

If you read the previous section, you'll know that this block results in a number of function calls. It cetainly looks readable (especially with syntax highlighting), but can we do better? I wouldn’t be bringing this up if I couldn't offer a better way, right?

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)

Loops are made for code reusability, and we saved quit a bit by leveraging .map function. The added cost is having to use the key property, but it still pays off.

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)

This is a huge win for both code readability and application size. Of course, it requires the Menu component to support this approach with a loop similar to above, which could add around 100 bytes, depending on the implementation. But the benefits are clear.

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 a 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

Arrow function counterpart could like like this:

let noop = () => {};
// 22 bytes minified (29% more bytes)

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 = () => ({}); return an empty object. It's a exception on the ugly side of JavaScript. See more here.

Destructuring With Care

I'm a huge fan of destructuring. 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

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)

A more traditional way to deal with repeated this keyword is referencing it.

function context() {
  let me = this;
  me.a();
  me.b();
  me.c();
  me.d();
}
// 55 bytes minified (2% improvement)

There's a catch with this method. You have to have at least 4 mentions of this to start seeing any improvement due to the fact that the initial reference to me ate up 12 bytes.

Tip: You can create a reference to any repeating keywords that can't be minified. This includes arguments, undefined, this, etc.

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

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)

The improvement is huge, staggering 48%! 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. In the current era of web development this comes at an expense of application size and often processing cycles.

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 optimizes as possible, where as 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.

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.

Grgur is a software architect at Modus Create, specializing in JavaScript performance, React JS, and Sencha frameworks. If this post was helpful, maybe his 16 years of experience with clients ranging from Fortune 100 to governments and startups could help your project too.