May 5, 2020

JavaScript optimization tips

Make JS optimizations to run faster

As a developer, we always look for ways to make our code faster and better.

But before that, writing high performance code requires three things:

  1. Know about the language and how it works
  2. Design based on the use case
  3. Debug! Fix! Repeat!

Remember this,

Any fool can write code that a computer can understand. Good programmers write code that humans can understand. - Martin Fowler

Let us see how to make JavaScript code to run even faster.

Be lazy

A lazy algorithm defers computation until it is necessary to execute and then produces a result.

const someFn = () => {
  doSomeOperation()
  return () => {
    doExpensiveOperation()
  }
}

const t = someArray.filter((x) => checkSomeCondition(x)).map((x) => someFn(x))

// Now execute the expensive operation only when needed.
t.map((x) => t())

TL;DR; The fastest code is the code that is not executed. So try to defer execution as much as possible.

Beware of object chaining

The JavaScript uses prototype inheritance. All the objects in the JavaScript world are instances of the Object.

The MDN says,

When trying to access a property of an object, the property will not only be sought on the object but on the prototype of the object, the prototype of the prototype, and so on until either a property with a matching name is found or the end of the prototype chain is reached.

For every property the JavaScript engine will have to go through the entire object chain until it finds a match. This is so resource intensive and hogs on your application performance if not used correctly.

So don't do this

const name = userResponse.data.user.firstname + userResponse.data.user.lastname

Instead do this

const user = userResponse.data.user
const name = user.firstname + user.lastname

TL;DR; Use temporary variable to hold the chained properties. Rather than repeatedly going through the chain.

Think before using transpilers

In the above case, the userResponse may or may not have the data object. That data object may or may not have the user property.

We can check that while getting the value like this

let name = ''
if (userResponse) {
  const data = userResponse.data
  if (data && data.user) {
    const user = data.user
    if (user.firstname) {
      name += user.firstname
    }
    if (user.lastname) {
      name += user.firstname
    }
  }
}

Well that is verbose. More the code, more the surface for bugs. Can we shrink it? of course, JavaScript has Optional chaining, destructuring assignment to make things less verbose.

const user = userResponse?.data?.user
const { firstname = '', lastname = '' } = user
const name = firstname + lastname

Isn't it slick? Modern? But beware when using things like this, the Babel transpile them as follows:

'use strict'

var _userResponse, _userResponse$data

var user =
  (_userResponse = userResponse) === null || _userResponse === void 0
    ? void 0
    : (_userResponse$data = _userResponse.data) === null ||
      _userResponse$data === void 0
    ? void 0
    : _userResponse$data.user
var _user$firstname = user.firstname,
  firstname = _user$firstname === void 0 ? '' : _user$firstname,
  _user$lastname = user.lastname,
  lastname = _user$lastname === void 0 ? '' : _user$lastname
var name = firstname + lastname

TL;DR; When using transpiling, make sure you choose the one that is more optimal for your use case.

Know SMI and heap numbers

Numbers are weird. The ECMAScript standardizes numbers as 64-bit floating-point values, also known as double precision floating-point or Float64 representation.

If the JavaScript engines store numbers in Float64 representation then it will lead to huge performance inefficiency. JavaScript Engines abstract the numbers such that its behavior matches Float64 exactly. The JavaScript engine executes integer operations much faster than compared to the float64 operations.

For more details, check this out.

TL;DR; Use SMI (small integers) wherever possible.

Evaluate Local Variables

Sometimes, folks think that it is readable to supply a value like this,

const maxWidth = '1000'
const minWidth = '100'
const margin = '10'
getWidth = () => ({
  maxWidth: maxWidth - margin * 2,
  minWidth: minWidth - margin * 2,
})

What if the getWidth function is called multiple times, the value is getting computed every time when you call it. The above calculation is not a big deal and you wouldn't notice any performance impact because of that.

But in general, lesser the evaluation at the runtime better the performance is.

// maxWidth - (margin * 2)
const maxWidth = '980'
// minWidth - (margin * 2)
const minWidth = '80'
const margin = '10'
getWidth = () => ({
  maxWidth,
  minWidth,
})

lesser the evaluation (at runtime) better the performance.

Use Map instead of switch / if-else conditions

Whenever you want to check multiple conditions, use a Map instead of switch / if-else condition. The performance of looking up elements in a map is much more faster than the evaluation of switch and if-else condition.

switch (day) {
  case 'monday':
    return 'workday'
  case 'tuesday':
    return 'workday'
  case 'wednesday':
    return 'workday'
  case 'thursday':
    return 'workday'
  case 'friday':
    return 'workday'
  case 'saturday':
    return 'funday'
  case 'sunday':
    return 'funday'
}

// or this

if (
  day === 'monday' ||
  day === 'tuesday' ||
  day === 'wednesday' ||
  day === 'thursday' ||
  day === 'friday'
)
  return 'workday'
else return 'funday'

Instead of both use this,

const m = new Map([
    ['monday','workday'],
    ['tuesday', 'workday'],
    ['wednesday', 'workday'],
    ['thursday', 'workday'],
    ['friday', 'workday'],
    ['saturday', 'funday'],
    ['sunday', 'funday']
];

return m.get(day);

TL; DR; Use Map instead of switch / if-else conditions

if-else ordering

For example if you are writing a React component, it is very common to follow this pattern.

export default function UserList(props) {
  const { users } = props

  if (users.length) {
    // some resource intensive operation.
    return <UserList />
  }

  return <EmptyUserList />
}

Here, we render <EmptyUserList /> when there are no users or render <UserList />. I have seen people argue that we have to handle all the negative scenarios at first and then handle the positive ones. They often come up with an argument, it is clearer for any one who reads it and also it is much more efficient. That is the following code is more efficient than the previous one.

export default function UserList(props) {
  const { users } = props

  if (!users.length) {
    return <EmptyUserList />
  }

  // some resource intensive operation
  return <UserList />
}

But what if the users.length always evaluate true. Use that first and then the negative condition.

TL;DR; While designing if-else condition, order them in such a way that the number of conditions evaluated is lesser.

Types are your best friends

JavaScript is both interpreted and compiled language. The compiler in order to produce more efficient binary requires type information. But being a dynamically typed language makes it difficult for the compilers.

The compilers when compiling the hot code (the code that is executed many times), makes some assumptions and optimise the code. The compiler spends some time to produce this optimised code. When these assumption fails, the compilers has to throw away the optimised code and fallback to the interpreted way to execute. This is time consuming and costly.

TL;DR; Use Monomorphic types always.

Others

Avoid recursion, sure they are awesome and more readable. But they also affect the performance.

Use memoization wherever and whenever possible.

Sometimes bitwise and unary operators give a slight edge in the performance. But they are really useful when your performance budget is very tight.


Up Next


யாதும் ஊரே யாவரும் கேளிர்! தீதும் நன்றும் பிறர்தர வாரா!!

@sendilkumarn