New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Profit from Tree Shaking in Ramda in Webpack #2355

Open
scabbiaza opened this Issue Oct 20, 2017 · 20 comments

Comments

Projects
None yet
8 participants
@scabbiaza

scabbiaza commented Oct 20, 2017

Hello, guys!

During my experiments with Tree Shaking in Ramda I found that the maximum profit I can get from it is the reducing a bundle on about 7Kb.

I'm wondering, is this because of the dependencies in Ramda or because of Webpack, or something else?

Here is how I tested it.
I created a file with the next code

import * as R from "ramda"

R.identity()

And compiled it two times:
with ramda@0.24 the size of the bundle was 59.2 kB
and with ramda@0.25 - 51.2 kB.

In my real project, where I did the same experiment, four bundles that use Ramda decreased their size on about 7Kb.

On the other hand, when I imported identity function directly from the source file, like this:

import identity from "ramda/src/identity"

identity()

size of the bundle was only 916 bytes. That kinda result I was expecting from Tree Shaking.

Code and Webpack configurations I uploaded to this repo:
https://github.com/scabbiaza/ramda-webpack-tree-shaking-examples

@benji6

This comment has been minimized.

Contributor

benji6 commented Oct 20, 2017

I've recently spent a lot of time debugging a very similar problem. The way Webpack tree-shaking works is it will still output the unused exports but leave it to the minifier to eliminate them. In the case I was looking at it wasn't eliminating any of the unused exports because the exported modules were accessing or setting properties which the minifier realised could potentially (via getters and setters) cause side-effects.

Without looking into it properly I would suspect that the unused exports aren't being cleaned up because they are often being wrapped by another function (often some sort of curry function) and as far as the minifier is aware those function invocations could have side-effects so it just leaves them.

@CrossEye

This comment has been minimized.

Member

CrossEye commented Oct 20, 2017

I think this is worth discussing in terms of better modularization of Ramda. I'm adding it to the list of items under discussion by the core team.

@buzzdecafe

This comment has been minimized.

Member

buzzdecafe commented Oct 20, 2017

thanks for the report @scabbiaza that is good info.

@Andarist

This comment has been minimized.

Contributor

Andarist commented Oct 21, 2017

Love the detailed report! I've created rollup example - scabbiaza/ramda-webpack-tree-shaking-examples#1

Resulting bundle - 513 bytes (that is umd wrapper included).

Ramda at the moment is suited for tree shaking as far as it can. Unfortunately webpack with its bug and its poor algorithm (not covering many cases) is not doing well here.

I've described some findings in comments under the refactoring PR, which I've discovered while working on it. Please read my comments there, starting from this one. However I see you probably already know most of the things Im describing there.

There is also a note about upcoming webpack4 which should help a lot in ramda's case, thanks to the "sideEffects": false entry in package.json.

And finally - after my findings I've created an issue on webpack's board, but to this very day I didn't get any kind of answer unfortunately.

PS. If you "manually cherry-pick" you should import from the es dir (when using ramda 0.25+ and module aware bundler), rather than src

@scabbiaza

This comment has been minimized.

scabbiaza commented Oct 22, 2017

@benji6, Webpack (actually not Webpack, but a minifier) does eliminate some functions.
For instance, in my example with using only identity, such functions as composeP or splitEvery were not present in the resulting bundle. However many more other were, like pipe, compose and so on.

@buzzdecafe, thank you! I'm happy to hear it.

@Andarist, thank you for the example with Rollup! The result is impressive.
Looks like the reason of the pure minification is in Webpack, not in Ramda.

@Andarist

This comment has been minimized.

Contributor

Andarist commented Oct 22, 2017

I've created yet another PR to your repository with ModuleConcatenationPlugin in webpack's config. It also helps a lot for webpack's case, I don't quite think it's webpack's merit in this case - it's that the scope hoisted bundle is more easily dead code eliminated by UglifyJS.

Output bundle - 877 B

EDIT:// If we pass { compress: { passes: 3 } } to the uglifyJs plugin we can go further down to 736 B

@scabbiaza

This comment has been minimized.

scabbiaza commented Oct 23, 2017

@Andarist , thank you for PR!

Yes, a bundle does reduce dramatically and the base example works perfectly.

However, need to try this approach on a big project, because this note in ModuleConcatenationPlugin documentation makes me worry:
These wrapper functions made it slower for your JavaScript to execute in the browser.

@Andarist

This comment has been minimized.

Contributor

Andarist commented Oct 23, 2017

@scabbiaza This comment is about webpack without ModuleConcatenationPlugin used.

@scabbiaza

This comment has been minimized.

scabbiaza commented Oct 23, 2017

I see, thank you! I was confused.
In this case, it should be the best solution on Webpack.

@Andarist

This comment has been minimized.

Contributor

Andarist commented Oct 23, 2017

Keep in mind that ModuleConcatenationPlugin is considered experimental at this point (i think, maybe its already past that phase). Im not sure how well it plays with code-splitted project, but Im using it without any problems with a single bundle app.

@Andarist

This comment has been minimized.

Contributor

Andarist commented Oct 23, 2017

Also - I really love the way you have created your examples repository. You could easily expand it to some medium writeup (not much longer than the existing README) and share with the community (I suppose posts reach broader audience than repositories). This is quite hot topic in the community, but I think most people do not realise what techniques can be used to leverage tree-shaking etc. More educational resources are needed and this would be a great one!

@scabbiaza

This comment has been minimized.

scabbiaza commented Oct 23, 2017

It's a good idea to write an article about it! Maybe I will :)

@polytypic

This comment has been minimized.

polytypic commented Oct 30, 2017

Note that at least with Rollup, the concatenation of all modules into a single scope is highly beneficial for the minification of the bundle. For example, in a React application using JSX, you typically have tons of calls to React.createElement, where React is an imported module. After Rollup concatenates all the modules, including React, to a single scope, React.createElement effectively becomes just createElement and the minifier can then automatically rename createElement to something short like h. This can reduce the bundle size significantly. The same applies to all module qualified references: SomeModule.someName can be shortened (easily by a minifier) after concatenation.

@djizco

This comment has been minimized.

djizco commented Nov 2, 2017

I've tested Using these 4 different syntax with Ramda 0.25 and webpack (3.8.1) treeshaking in a production build with module concatenation. These were my results.

import * as R from 'ramda';
// No tree shaking, 318 modules ~52kb
 
import { identity } from 'ramda'
// No tree shaking, 318 modules ~52kb

import { identity } from 'ramda/es';
// No tree shaking, 318 modules ~52kb

import identity from 'ramda/es/identity';
// This was the only one that worked. 4 modules - 302B

From what I can tell the only way currently to benefit from tree shaking is to import modules individually.

@Andarist

This comment has been minimized.

Contributor

Andarist commented Nov 2, 2017

As explained here first three options you have presented are basically the same - they do exactly the same thing.

Please follow related discussions (they should all be linked in this thread) - its just how webpack is currently working (meaning poorly in those regards). Other tools like rollup gives better results, but keep also in mind that ramda is a special case because of the heavy usage of higher order functions which are not easily tree-shakeable anyway.

For better webpack's results you can use its ModuleConcatenationPlugin, although it still won't give perfect output, it will be by far better.

@guillaumearm

This comment has been minimized.

guillaumearm commented Mar 17, 2018

I have made some investigations on this, related to char0n/ramda-adjunct#456.

Even if I use babel (no webpack), "module" field (package.json) resolution does not works.

ramda@0.25.0
babel@6
{
  "presets": [
    ["es2015"],
    ["stage-0"]
  ]
}

If I remove node_modules/ramda/src folder, and let node_modules/ramda/es, this doesn't works.

import R from 'ramda' // always use "main" field (commonjs)
import * as R from 'ramda' // always use "main" field (commonjs)

Anyway, it works fine with a "jsnext:main" field in package.json, but "module" seems to be ignored by babel.

{
  "jsnext:main": "./es/index.js"
}

Any ideas ?

@Andarist

This comment has been minimized.

Contributor

Andarist commented Mar 17, 2018

Both "jsxnext:main" and "module" have nothing to do with babel. Babel is just a transpiler and it doesn't resolve your modules, it operates on single files only.

Those fields are targeting bundlers such as webpack and rollup and their resolution algorithms.

You'd have to share a repository illustrating the problem, so I could look at it and point out the problem more quickly.

@guillaumearm

This comment has been minimized.

guillaumearm commented Mar 18, 2018

I don't know why I thought babel-preset-es2015 have a resolver for import, but effectively it's just transform import syntax into require.

import R from 'ramda';

will be replaced by something like:

var R = require('ramda')

So commonJS bundle is used here, everything is fine.
I think my incomprehension come with eslint-plugin-import errors I'd have.

@Andarist

This comment has been minimized.

Contributor

Andarist commented Mar 18, 2018

So the case is closed, right? Please remember about closing the issue on ramda-adjunct board 😃 Cheers!

@guillaumearm

This comment has been minimized.

guillaumearm commented Mar 18, 2018

I'm not sure for ramda-adjunct issue, I'd already have this case :

import R from 'ramda' // OK
import RA from 'ramda-adjunct' // undefined
import * as RA from 'ramda-adjunct' // OK

I have to take a little time to see what is going on here.

Anyway, Ramda imports are good to me, sorry for the inconvenience.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment