Ramda, webpack, and tree shaking.

Are you using Ramda and Webpack? Then you probably want to read this, because chances are good you're importing more of the Ramda library than you might think...

We're all big on reducing bundle sizes for the web. When leveraging larger utility libraries such as Ramda, we are encouraged to only import the functions we need:

import R from 'ramda' // bad times  
import { identity } from 'ramda' // good times  

At Transparent Classroom, we dutifully do as such, yet recently I was struck with an unfortunate reality: the whole Ramda library was still being imported.

app bundle size

So, I started to do some investigating, and began with a simple test file:

import { identity } from 'ramda'

identity()  

Running webpack on that, it yielded this result, which felt like a horrible betrayal.

simplified version

The expected contract was broken: when you extract your imports, you don't get the whole darn library. Based on several discussions (here, here, and here), most fingers point to webpack being the culprit. There was a thorough exploration of webpack and its tree shaking abilities WRT Ramda done here. It turns out your options are limited if you're just using webpack:

  1. Manually cherry pick: do import identity from 'ramda/es/identity' -- ugh, no thank you.
  2. Use ModuleConcatenationPlugin -- helps improve the tree shaking algorithm, though it's still a bit experimental.

Fortunately, if you use babel, there's a handy plugin that can manually turn your general imports into cherrypicked imports:

import R, {map} from 'ramda';

map(R.add(1), [1, 2, 3]);  

becomes

import add from 'ramda/src/add';  
import map from 'ramda/src/map';

map(add(1), [1, 2, 3]);  

With this plugin in place, justice is restored:

simplified after plugin

Doing this on our app reduced the Ramda library import by 60%, from 75.5KB to 29.8KB.

app after plugin

However, I still like to include the whole Ramda library and set it on window in my dev environment for debugging purposes, i.e. window.R = require('ramda'). Turns out the plugin doesn't like that, discussed here. The easy solution is to only enable the plugin in dev. Here's my .babelrc file, shortened to the relevant part:

{
  ...
  "env": { "production": { "plugins": ["ramda"] } }
}

Next up: dropping use of lodash. Oh the joys of working on apps that have lived through many eras of web development (that is to say, is ~5 years old).