-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
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
Comments
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. |
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. |
thanks for the report @scabbiaza that is good info. |
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 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 |
@benji6, Webpack (actually not Webpack, but a minifier) does eliminate some functions. @buzzdecafe, thank you! I'm happy to hear it. @Andarist, thank you for the example with Rollup! The result is impressive. |
I've created yet another PR to your repository with Output bundle - 877 B EDIT:// If we pass |
@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 |
@scabbiaza This comment is about webpack without |
I see, thank you! I was confused. |
Keep in mind that |
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! |
It's a good idea to write an article about it! Maybe I will :) |
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 |
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. |
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 |
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.
{
"presets": [
["es2015"],
["stage-0"]
]
} If I remove 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 ? |
Both "jsxnext:main" and "module" have nothing to do with 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. |
I don't know why I thought import R from 'ramda'; will be replaced by something like: var R = require('ramda') So commonJS bundle is used here, everything is fine. |
So the case is closed, right? Please remember about closing the issue on ramda-adjunct board 😃 Cheers! |
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. |
Hi, I'm working on optimizing the What is its current state? It seems that the only importing method allowing tree shaking is: But I'm not seing any build output difference compared to the other 3 importing methods.
I would love some 2022 feedbacks. |
When i've optimized this stuff the lib became almost fully tree-shakeable when importing stuff from the root entrypoint. This was quite some time ago - cant say if there were any regressions in this area since then. If ur company care about it - i could make a paid audit of your code, your conclusions and Ramda. |
What do you mean by "importing stuff from the root entrypoint"? |
Yes, when I was testing this out this was sufficient to get almost perfect tree-shaking - there was no need to use deep imports like |
I just did some com tests with Next.js v12 :
It seems that's the only way to get a fully optimized build is via I was thinking |
Well, hard to say what's going on if you don't share full repro case that could be inspected and analyzed. |
@Andarist Here is the basic Next project used for my tests (cf. my previous message results). |
If you were using some bundle analyzer here - then it's broken. IIRC some of those were just hooked into webpack before certain optimizations were executed so they were always skewing results. If we actually inspect output files then we can roughly see everything that was included from Ramda: code from Ramda 6155: function (t, e, n) {
"use strict";
n.d(e, {
yRu: function () {
return y;
},
});
function r(t) {
return (
null != t &&
"object" === typeof t &&
!0 === t["@@functional/placeholder"]
);
}
function i(t) {
return function e(n) {
return 0 === arguments.length || r(n) ? e : t.apply(this, arguments);
};
}
Array.isArray;
"undefined" !== typeof Symbol && Symbol.iterator;
function o(t, e) {
return Object.prototype.hasOwnProperty.call(e, t);
}
var a = Object.prototype.toString,
c = (function () {
return "[object Arguments]" === a.call(arguments)
? function (t) {
return "[object Arguments]" === a.call(t);
}
: function (t) {
return o("callee", t);
};
})(),
u = c,
l = !{ toString: null }.propertyIsEnumerable("toString"),
s = [
"constructor",
"valueOf",
"isPrototypeOf",
"toString",
"propertyIsEnumerable",
"hasOwnProperty",
"toLocaleString",
],
f = (function () {
return arguments.propertyIsEnumerable("length");
})(),
d = function (t, e) {
for (var n = 0; n < t.length; ) {
if (t[n] === e) return !0;
n += 1;
}
return !1;
};
Object.keys, Number.isInteger;
"function" === typeof Object.is && Object.is;
var g = function (t) {
return (t < 10 ? "0" : "") + t;
};
Date.prototype.toISOString;
function p(t) {
return t;
}
var y = i(p);
"function" === typeof Object.assign && Object.assign;
var m =
"\t\n\v\f\r \xa0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029\ufeff";
String.prototype.trim;
}, There are, in fact, some things that wouldn't have to be here... but common, this code weighs something around 720bytes (gzipped). |
Thanks as always @Andarist for bringing your expertise to bear here. Some day, I'll dedicate some effort to learning all this stuff! |
@CrossEye it really isnt worth it 😂 |
@Andarist Thanks for taking a look. I'm using webpack-bundle-analyzer, but I will take a look a the code output of the build as you did 👍 So |
so @Andarist we should ignore @monsieurnebo recommendation for this syntax? |
According to my latest test - that wasn't a that long time ago - yes 😉 It's just that this stuff is notoriously hard to test in an automatic way. That's why various tools might report somewhat inaccurate data. |
treeshake with webpack and imports as |
Have you confirmed this manually? Those tools often are inaccurate |
@Andarist I can confirm it as well. You can verify it by this simple entry point with webpack@5: import { any } from 'ramda';
console.dir(any); |
You would have to prepare a runnable repro case for me to investigate this. |
We experience the same problem on a particular instance but does not seem reproducible in an isolated build: https://github.com/anacierdem/ramda-test Note that the output script is committed in |
Hi @Andarist, Managed to find a consistent time block to do some deeper research. The conclusion of the research is: ramda can properly tree-shake. ExplanationThe key to properly tree-shake ramda imports is to:
{
mode: 'production', // drops "dead code" from the bundle by setting proper defaults to `optimization` config
optimization: {
sideEffects: true, // tells webpack to recognise the sideEffects flag in package.json, ramda is side effects free
minimize: true, // needs to be set to `true` for proper tree-shaking
providedExports: true, // if set to `true` it gives far better results
usedExports: true, // needs to be set to `true` for proper tree-shaking
concatenateModules: true, // needs to be set to `true` for proper tree-shaking
}
} The repo where I did the research is available at https://github.com/char0n/ramda-tree-shaking-webpack. |
nice work @char0n , did you happen to find out whether all of these optimization fields are necessary given that webpack docs say that when mode is set to production it has some default optimizations? top of https://webpack.js.org/configuration/optimization/ |
@damiangreen as mentioned in my research notes, you either use {
mode: 'production', // drops "dead code" from the bundle by setting proper defaults to `optimization` config
optimization: {
sideEffects: true, // tells webpack to recognise the sideEffects flag in package.json, ramda is side effects free
minimize: true, // needs to be set to `true` for proper tree-shaking
providedExports: true, // if set to `true` it gives far better results
usedExports: true, // needs to be set to `true` for proper tree-shaking
concatenateModules: true, // needs to be set to `true` for proper tree-shaking
}
} So in ramda terminology: the bullet list two last items form disjunction and not conjunction ;]
|
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
And compiled it two times:
with
ramda@0.24
the size of the bundle was 59.2 kBand 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: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
The text was updated successfully, but these errors were encountered: