Tree shake Lodash with Webpack, Jest and Typescript

Martin Hochel
8 min readJun 19, 2017

So recently, I’ve started to do payload optimizations within one of our large web apps at work. During that process, I came across very serious issue, well, with lodash while achieving to have everything running smoothly ( tree shaking, jest unit tests and correct type definitions )

I had 3 goals:

  • production bundle with just those lodash functions, that are used ( not the whole library )
  • typescript valid code without errors ( correct lodash typings )
  • all tests passing

Our stack used at that particular project is following:

  • preact + preact-compat ( renderer )
  • redux + redux-observable + rx ( state management )
  • axios ( http req/res )
  • typescript 2.x ( for both type checking and transpiling to ES5)
  • webpack 2.x ( for bundling )
  • jest 20.x + ts-jest ( unit testing + snapshots )
  • lodash 4.x ( just few functions for some functionality )
  • and some other 3rd party libs…

Backstory

Webpack is ES2015 modules aware, so we are not transpiling import/export to ES5. This is achieved by having tsconfig.json config like this:

{ module: es2015, target: es5 }

Lodash doesn’t ship with types unfortunately, so we have to install them from npm, via yarn add -D @types/lodash

With this setup, we have now lodash ready to go in our typed javascript. yasss!

Okay, so let’s say, we wanna leverage _.get for obtaining values by path, from complicated object ( if you’re doing this often === sign of code smell that your data are badly structured ).

Let’s look at some example from our codebase

Note: following example is highly contrived, just for demonstration purposes of using lodash.

import { get } from 'lodash'import { usersService } from '../core'const main = () => {
userService
.getAll()
.then(data => get(data.response,'[3].address.zip'))
.then(userZip => dispatch(userZipReceived(userZip))
}
main()

Now when we run webpack --env.prod for creating production minified and tree shaked bundle, there is some huge discrepancy.

130kb of minified lodash in your bundle ( WHAT?! ) just for using one function from the library…

In theses situations I like to use a very accurate phrase coined by Martin Probst from angular team used during chaotic angular 2 RC release phase:

OH NO! PANIC!

Keep calm yo, there is a solution for sure! lodash-es ! oh is it?

Okay so yarn add lodash-es && yarn add -D @types/lodash-es

lodash-es stands for, you guess it, es2015 modules aware lodash, so now, there is nothing in our/webpack way to get proper tree shaking!

Let’s run build and profile our bundle with webpack-bundle-analyzer

bang! whole lodash in your production bundle

OH NO PANIC #2! SAME HUGE SIZE !

So can I tree shake lodash or what ???

Let’s say there are multiple solutions…. huh… multiple you say?!

there are 3 solutions…

Solution 1 ( non feasible for our use-case )

After little googling you will find this issue https://github.com/webpack/webpack/issues/1750 which will tell you that you have to use babel-plugin-lodash for some magic transformation under the hood. Easy right?

Oh wait we are not using babel, so let’s modify our build pipeline by introducing all babel dependencies and plugins for transpilation and use typescript for type checking ( yes you can do just that ), and you are good to go.

Ofc, don’t remember to update your jest configuration appropriately cowboy!

Sad Truth ( for lodash users ):

Without using babel and babel-plugin-lodash there is currently no way how to tree shake lodash, if you wanna use concise import without subpaths => import { get } from 'lodash-es'

Solution 2 ( no babel )

Use subpath imports from lodash with Typescript

install lodash, @types/lodash, @types/lodash-es

Just useimport get from 'lodash/get' and you’re good to go!

Whoops not so easy cowboy !

You will get TS errors and your test will start to fail like this:

Typescript errors when using subpath with lodash
Tests are failing

Why errors ?

→ TS errors:

bad subpath typings provided by @types/lodash

Test errors:

ts-jest transforms our TS code to ES5 + commojs module format for Jest, so import get from 'lodash/get' is transiled to commonjs style with default property, which of course, doesn’t exist within lodash source ( there is just module.exports = get .

Transpiled TS to ES5/CommonJS by ts-jest which is consumed by Jest -> Error lodash has no default exports rather than module.exports
lodash/get.js → common js, no es2015 default export

How to solve this ?

Solution is quite easy, you need to add following config to tsconfig.json:

{
"allowSyntheticDefaultImports": true,
"baseUrl": ".",
"typeRoots": [
"node_modules/@types",
"manual_typings"
],
"paths": {
"lodash/*": [
"node_modules/@types/lodash-es/*"
],

},
}
  • “typeRoots” && “paths”:

by using typeRoots we are telling Typescript compiler to look to different folders for 3rd party typings. By default TS looks for types at node_modules/@types.

paths are used for explicitly overriding path resolution algorithm. In our case we are overriding lodash module path to lodash-es which has correct subpath Type definitions. Note that you have to use “baseUrl” when using path

Result:

tree shaked lodash in production bundle

TS without errors
tests are green
tree shaked lodash in production bundle

Solution 3 ( no babel )

Use subpath imports from lodash-es with Typescript

install lodash-es, @types/lodash-es

Just useimport get from 'lodash-es/get' and you’re good to go!

Whoops not so easy cowboy ! ( huh Deja vu ? )

This time no TS errors, nice!

lodash-es provides correct subpath types

but your test will start to fail like this:

Failing test when using lodash-es

Why errors ?

→ Test errors:

We are now using lodash-es which uses ES2015 modules. Jest by default doesn’t apply transformers to any node_modules package for performance reasons, so yeah import really isn’t commonjs or valid ES5 code.

How to solve this ?

We need to transpile lodash-es source code explicitly in jest and enable vanilla JS files transpilation by typescript

jest.config.json:

"transform": {"^.+\\.(j|t)sx?$": "<rootDir>/node_modules/ts-jest/preprocessor.js",},"transformIgnorePatterns": [
"<rootDir>/node_modules/(?!lodash-es/.*)"
],
  • ^.+\\.(j|t)sx?$

We have to transpile also .js files not just Typescript

We have to white list lodash-es within transformIgnorePatterns

We have now setup jest, last thing to do is to tell Typescript to transpile .js files

tsconfig.json: — no excessive hacks and overrides like in previous solution. LGTM !

{
"allowJs": true
}

Result:

tree shaked lodash in production bundle

tests are green
No Typescript errors
correctly tree shaked lodash-es in production bundle

lodash size ( non gzipped ) in our prod bundle ~= 30kb which is 100kb less than before! We are done here!

Takeaways:

  • We ended up using solution #3( although on bundle-analyzer it looks like that solution #2 has smaller footprint, but with solution 3 our final vendor bundle is 20kB smaller ). Also we prefer to use es2015 ready modules for further optimizations and another benefit is that we don’t have to hack types.
  • Jest is freakin’ awesome!, but! you have to always double config stuff (webpack and jest ) which feels MEH ( now you need to hire both webpack config senior developer and jest config senior dev :D )
  • Always remember that when you consume npm package which has “module” field -> webpack understands that, but if the package is missing proper UMD/commonjs you will have to transpile it as well within Jest
  • 3rd party types are not always excellent ( also DefinitelyTyped type versions doesn’t always match with used library version, so you’ve absolutely no guarantees, that you have actual and correct types for your particular version of 3rd party package => for that reason I always prefer 3rd party libs that are written in Typescript or ship type definitions directly. In edge cases you can modify/create custom types and use technique that was introduced in Solution 2)
  • Have to solve all this issues in 2017, feels like a huge tax payed for using only few lodash functions. I’ll probably use other, tree shake ready and more functional solution on next project ( yup I’m looking at you Ramda ! )

YES I KNOW: lodash/fp is solution as well, but there are absolutely no type definitions for that currently ( June 2017 ), so a no go for me and my team

Hope this will be useful for anyone who may struggle with similar problem now or in the future.

Happy hunting and may the force be with you folks! Cheers!

UPDATE August 2017:

Webpack (

) is rockin’ hard and with the new pure-module feature, lodash will be tree-shake able by default, so no sub paths import will be needed.

import {get,pick} from 'lodash-es' will just work! more info :

UPDATE January 2019:

TypeScript added esModuleInterop which properly resolves non standard default imports, which acts similarly like babel. Also ts-jest went through various changes, so the ultimate solution is now much easier than before ❤️:

// install lodash (both commonjs nad es2015 modules) with types
yarn add lodash-es
// commonjs lodash will be used for tests
yarn add -D lodash @types/lodash-es
// jest.config.jsconst config = {
preset: 'ts-jest',
moduleNameMapper: {
// we'll use commonjs version of lodash for tests 👌
// because we don't need to use any kind of tree shaking right?!
'^lodash-es$': '<rootDir>/node_modules/lodash/index.js'
},
}

// tsconfig.json
{
compilerOptions: {
// other config settings
"esModuleInterop": true,
// no allowJs needed 💥🚨👌
}
}

Now following works as expected:

import {get,pick} from 'lodash-es'

--

--

Martin Hochel

Principal Engineer | Google Dev Expert/Microsoft MVP | @ngPartyCz founder | Speaker | Trainer. I 🛹, 🏄‍♂️, 🏂, wake, ⚾️& #oss #js #ts