Mirko's blog

Doing more with less lodash

Sat, Aug 29, 2015

A while back I was looking into reducing the size of the Javascript bundle in our new order process.

We use Webpack to build our bundle. After perusing the docs I found out that I can create a “profile” of our bundle by running

webpack --profile --json > profile.json

The profile is not exactly human-readable so to make sense of things I used an npm module called webpack-bundle-size-analyzer

analyze-bundle-size profile.json

This produces a listing like

lodash: 238.41 kB (28.5%)
  <self>: 238.41 kB (100%)
baconjs: 89.77 kB (10.7%)
  <self>: 89.77 kB (100%)
markdown: 49.91 kB (5.97%)
  <self>: 49.91 kB (100%)
webpack: 17.64 kB (2.11%)
...more modules...

Yikes! lodash is taking up a huge amount of space in our app. The sizes are unminifies and uncompressed so a well-commented project like lodash looks extra-bad when viewed this way. In any case the sizes in the profile are a pretty good indicator on what is taking space in the bundle.

Out with the old, in with the new

We were using lodash 2.4.x, which was the “classic” build contained in one huge file which was therefore included in our build whether we used all of it or not.

I figured that by upgrading to the “modern” 3.x branch and only importing the bits we are actually using there might be some size reduction to be gained.

The next step was to figure out what we are using lodash for and replacing the import _ from ‘lodash’ statements by something like import cloneDeep from ‘lodash/lang/cloneDeep’.

I used The Silver Searcher aka “ag” and Vim to find and edit the files.

vim $(ag "import\s+_\s+from.+lodash" --js -l)

Most of the work was just simple replacement, turning blocks like

import _ from 'lodash'
let myCopy = _.cloneDeep(someObject)
into
import cloneDeep from 'lodash/lang/cloneDeep'
let myCopy = cloneDeep(someObject)

ES2015 instead of lodash

When going through the code I discovered that many of our lodash uses were not necessary anymore after migrating to ES2015.

Things like _.map(), _.find() etc. have obvious replacements in native arrays but there was more.

Using ES2015 destructuring to replace _.pluck()

Before:

let messages = [{type: 'error', msg: 'Foo'}, {type: 'info', msg: 'Bar'}]
_.pluck(messages, 'type').map(function(type) { ... })

After:

let messages = [{type: 'error', msg: 'Foo'}, {type: 'info', msg: 'Bar'}]
messages.map(({type}) => ... )

Iterating objects with Object.entries instead of _.map()

_.map() is very handy for iterating Objects but ES2015 makes it obsolete.

Before:

let smileys = { ':D': '😃', ':P': '😛' }
_.map(objs, (emoji, text) => { ... })

After:

let smileys = { ':D': '😃', ':P': '😛' }
Object.entries(smileys).map(([text, emoji]) => { ... })

Using Object.assign() instead of _.merge()

There are a few places in the code where an object is updated by merging the changes into it. Replacing these with Object.assign() seems straightforward.

Before:

let person = { name: "John Doe",
    address: { street: "Somestreet", city: "Somecity" }
}
// Change street in address
_.merge(person, { address: { street: "Otherstreet" } })

After:

let person = { name: "John Doe",
    address: { street: "Somestreet", city: "Somecity" }
}
// Change street in address
Object.assign(person, { address: { street: "Otherstreet" } })

Ok, simple change, right? I thought so for a while. Then I noticed that the app was behaving a little differently. Then I realized the mistake.

Object.assign() does really what is says. _.merge() merges the object while Object.assign() just overwrites whatever was occupying the keys in the target object. In this case it replaced the address object within person completely.

One could argue that update-by-merging is a bad idea to begin with (and I would agree) but the difference is small enough to go unnoticed in many use cases.

I resolved the issue by importing and using the merge function from lodash in the cases object merging was actually needed.

Final results

After the last reference to the lodash underscore global was gone I ran the profile command again and the results were pretty good.

baconjs: 89.77 kB (13.1%)
  <self>: 89.77 kB (100%)
lodash: 80 kB (11.7%)
  <self>: 80 kB (100%)
markdown: 49.91 kB (7.28%)
  <self>: 49.91 kB (100%)
webpack: 17.64 kB (2.57%)
...more modules...

Lodash not takes dramatically less space, going down from 238k to 80k. In the minified build the difference is around 33k, which is awesome.

Summary

Lodash is nice but often only a handful of it’s functions are required. Using the components of the modern build reduces bundle size significantly. Use ES2015 instead of lodash when possible. And it very often is.

Comments

Comments