One of the benefits of working with smart people is that you can learn a lot from them through osmosis. As luck would have it, a recent move placed my office next to John-David Dalton‘s, with the perk being that he occasionally wanders into my office to talk about cool stuff he’s working on, like Lodash and ES modules in Node.
Recently we chatted about Lodash and the various plugins for making its bundle size smaller, such as lodash-webpack-plugin and babel-plugin-lodash. I admitted that I had used both projects but only had a fuzzy notion of what they actually did, or why you’d want to use one or the other. Fortunately J.D. set me straight, and so I thought it’d be a good opportunity to take what I’ve learned and turn it into a short blog post.
TL;DR
Use the import times from 'lodash/times'
format over import { times } from 'lodash'
wherever possible. If you do, then you don’t need the babel-plugin-lodash
. Update: or use lodash-es instead.
Be very careful when using lodash-webpack-plugin
to check that you’re not omitting any features you actually need, or stuff can break in production.
Avoid Lodash chaining (e.g. _(array).map(...).filter(...).take(...)
), since there’s currently no way to reduce its size.
babel-plugin-lodash
The first thing to understand about Lodash is that there are multiple ways you can use the same method, but some of them are more expensive than others:
import { times } from 'lodash' // 68.81kB :( import times from 'lodash/times' // 2.08kB! :) times(3, () => console.log('whee'))
You can see the difference using something like webpack-bundle-analyzer. Here’s the first version:
Using the import { times } from 'lodash'
idiom, it turns out that lodash.js
is so big that you can’t even see our tiny index.js
! Lodash takes up a full parsed size of 68.81kB. (In the bundle analyzer, hover your mouse over the module to see the size.)
Now here’s the second version (using import times from 'lodash/times'
):
In the second screenshot, Lodash’s total size has shrunk down to 2.08kB. Now we can finally see our index.js
!
However, some people prefer the second syntax to the first, especially since it can get more terse the more you import.
Consider:
import { map, filter, times, noop } from 'lodash'
compared to:
import map from 'lodash/map' import filter from 'lodash/filter' import times from 'lodash/times' import noop from 'lodash/noop'
What the babel-plugin-lodash
proposes is to automatically rewrite your Lodash imports to use the second pattern rather than the first. So it would rewrite
import { times } from 'lodash'
as
import times from 'lodash/times'
One takeway from this is that, if you’re already using the import times from 'lodash/times'
idiom, then you don’t need babel-plugin-lodash
.
Update: apparently if you use the lodash-es package, then you also don’t need the Babel plugin. It may also have better tree-shaking outputs in Webpack due to setting "sideEffects": false
in package.json
, which the main lodash
package does not do.
lodash-webpack-plugin
What lodash-webpack-plugin
does is a bit more complicated. Whereas babel-plugin-lodash
focuses on the syntax in your own code, lodash-webpack-plugin
changes how Lodash works under the hood to make it smaller.
The reason this cuts down your bundle size is that it turns out there are a lot of edge cases and niche functionality that Lodash provides, and if you’re not using those features, they just take up unnecessary space. There’s a full list in the README, but let’s walk through some examples.
Iteratee shorthands
What in the heck is an “iteratee shorthand”? Well, let’s say you want to map()
an Array of Objects like so:
import map from 'lodash/map' map([{id: 'foo'}, {id: 'bar'}], obj => obj.id) // ['foo', 'bar']
In this case, Lodash allows you to use a shorthand:
import map from 'lodash/map' map([{id: 'foo'}, {id: 'bar'}], 'id') // ['foo', 'bar']
This shorthand syntax is nice to save a few characters, but unfortunately it requires Lodash to use more code under the hood. So lodash-webpack-plugin
can just remove this functionality.
For example, let’s say I use the full arrow function instead of the shorthand. Without lodash-webpack-plugin
, we get:
In this case, Lodash takes up 18.59kB total.
Now let’s add lodash-webpack-plugin
:
And now Lodash is down to 117 bytes! That’s quite the savings.
Collection methods
Another example is “collection methods” for Objects. This means being able to use standard Array methods like forEach()
and map()
on an Object, in which case Lodash gives you a callback with both the key and the value:
import forEach from 'lodash/forEach' forEach({foo: 'bar', baz: 'quux'}, (value, key) => { console.log(key, value) // prints 'foo bar' then 'baz quux' })
This is handy, but once again it has a cost. Let’s say we’re only using forEach
for Arrays:
import forEach from 'lodash/forEach' forEach(['foo', 'bar'], obj => { console.log(obj) // prints 'foo' then 'bar })
In this case, Lodash will take up a total of 5.06kB:
Whereas once we add in lodash-webpack-plugin
, Lodash trims down to a svelte 108 bytes:
Chaining
Another common Lodash feature is chaining
, which exposes functionality like this:
import _ from 'lodash' const array = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10'] console.log(_(array) .map(i => parseInt(i, 10)) .filter(i => i % 2 === 1) .take(5) .value() ) // prints '[ 1, 3, 5, 7, 9 ]'
Unfortunately there is currently no good way to reduce the size required for chaining. So you’re better off importing the Lodash functions individually:
import map from 'lodash/map' import filter from 'lodash/filter' import take from 'lodash/take' const array = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10'] console.log( take( filter( map(array, i => parseInt(i, 10)), i => i % 2 === 1), 5) ) // prints '[ 1, 3, 5, 7, 9 ]'
Using the lodash-webpack-plugin
with the chaining
option enabled, the first example takes up the full 68.81kB:
This makes sense, since we’re still importing all of Lodash for the chaining to work.
Whereas the second example with chaining
disabled gives us only 590 bytes:
The second piece of code is a bit harder to read than the first, but it’s certainly a big savings in file size! Luckily J.D. tells me there may be some work in progress on a plugin that could rewrite the second syntax to look more like the first (similar to babel-plugin-lodash
).
Edit: it was brought to my attention in the comments that this functionality should be coming soon to babel-plugin-lodash
!
Gotchas
Saving bundle size is great, but lodash-webpack-plugin
comes with some caveats. By default, all of these features – shorthands
for the iteratee shorthands, collections
for the Object collection methods, and others – are disabled by default. Furthermore, they may break or even silently fail if you try to use them when they’re disabled.
This means that if you only use lodash-webpack-plugin
in production, you may be in for a rude surprise when you test something in development mode and then find it’s broken in production. In my previous examples, if you use the iteratee shorthand:
map([{id: 'foo'}, {id: 'bar'}], 'id') // ['foo', 'bar']
And if you don’t enable shorthands
in lodash-webpack-plugin
, then this will actually throw a runtime error:
map.js:16 Uncaught TypeError: iteratee is not a function
In the case of the Object collection methods, it’s more insidious. If you use:
forEach({foo: 'bar', baz: 'quux'}, (value, key) => { console.log(key, value) })
And if you don’t enable collections
in lodash-webpack-plugin
, then the forEach()
method will silently fail. This can lead to some very hard-to-uncover bugs!
Conclusion
The babel-plugin-lodash
and lodash-webpack-plugin
packages are great. They’re an easy way to reduce your bundle size by a significant amount and with minimal effort.
The lodash-webpack-plugin
is particularly useful, since it actually changes how Lodash operates under the hood and can remove functionality that almost nobody uses. Support for edge cases like sparse arrays (guards
) and typed arrays (exotics
) is unlikely to be something you’ll need.
While the lodash-webpack-plugin
is extremely useful, though, it also has some footguns. If you’re only enabling it for production builds, you may be surprised when something works in development but then fails in production. It might also be hard to add to a large existing project, since you’ll have to meticulously audit all your uses of Lodash.
So be sure to carefully read the documentation before installing the lodash-webpack-plugin
. And if you’re not sure if you need a certain feature, then you may be better off enabling that feature (or disabling the plugin entirely) and just take the ~20kB hit.
Note: if you’d like to experiment with this yourself, I put these examples into a small GitHub repo. If you uncomment various bits of code in src/index.js
, and enable or disable the Babel and Webpack plugins in .babelrc
and webpack.config.js
, then you can play around with these examples yourself.
Posted by Luke on March 20, 2018 at 12:04 PM
Re chaining there is some support in the Babel plugin, awaiting release
https://github.com/lodash/babel-plugin-lodash/pull/176
Posted by Nolan Lawson on March 20, 2018 at 2:00 PM
Thanks! I’ll update the post. :)
Posted by inoyakaigor on March 22, 2018 at 1:40 PM
How to reduce bundle size?
Do not use functions that already exist in Javascript!
Posted by Sky on March 23, 2018 at 10:26 AM
Hi Nolan why you do not use lodash-es?
Posted by Nolan Lawson on March 29, 2018 at 10:55 AM
Just learned about this; apparently it obviates the need for the Babel plugin or for remembering to always do
import times from 'lodash/times'
. I’ll make a note in the post, thanks!Posted by pixelcog on March 23, 2018 at 9:21 PM
I’m pretty sure with webpack 4’s support for pure modules and tree shaking, you can just
import { times } from 'lodash-es';
and it will work the same way (without the need for extra plugins or babel transformsPosted by Nolan Lawson on March 29, 2018 at 10:59 AM
Thanks, just learned about
lodash-es
and so I updated the post! :)Posted by itsu on March 28, 2018 at 6:48 AM
Posted by garethotwell on October 2, 2018 at 1:02 AM
Will this approach work for Webpack 3? Our company codebase is tightened to this Webpack version, so update to Webpack 4 is not the option. Anyway thank you for the post. Will try to use lodash-es and hopefully to have smaller builds!