-
-
Notifications
You must be signed in to change notification settings - Fork 2.3k
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
[RFC] Support module concatenation (scope hoisting) #1104
Comments
Great write up! Related: #392 |
Seems like minification and concatenation should happen in the packager for this feature? This definitely seems like an easier and more reliable option than writing a custom treeshaker. |
I think much of this can be done at the module level inside a worker. If we rename all variables in the top-level scope of a module in a predictable way like @fathyb described (e.g. We'd need to take special care to handle Here are the cases where this would not be possible:
Some cases where module concatenation alone is not enough:
Aside from the above issues, I think this would be a great start on our way to tree shaking! 🎉 |
I started a branch for this: scope-hoist. Obviously still a lot to do but the basics seem to work. It's implemented as a babel transform, and it happens entirely in the worker process. There is currently a new replacement JS packager that does the concatenation and final variable replacements to link modules together. This may get more complicated and get merged back into the main JSPackager, not sure yet. As an example, here is what happens with the following input: import leftpad from 'leftpad';
function add(a, b) {
return a + b;
}
export default leftpad(add(1, 2) + 4, 10);
console.log(module.exports); Output: (function () {
var $3$exports = {};
$3$exports = function (str, width, char) {
char = char || "0";
str = str.toString();
while (str.length < width) str = char + str;
return str;
};
'use strict';
var $1$exports = {};
Object.defineProperty($1$exports, "__esModule", {
value: true
});
var $1$var$_leftpad = $3$exports;
var $1$var$_leftpad2 = $1$var$_interopRequireDefault($1$var$_leftpad);
function $1$var$_interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function $1$var$add(a, b) {
return a + b;
}
$1$exports.default = (0, $1$var$_leftpad2.default)($1$var$add(1, 2) + 4, 10);
console.log($1$exports);
})(); When you use import leftpad from 'leftpad';
function add(a, b) {
return a + b;
}
eval('exports.foo = "bar"');
export default leftpad(add(1, 2) + 4, 10);
console.log(module.exports); Output: (function () {
var $3$exports = {};
$3$exports = function (str, width, char) {
char = char || "0";
str = str.toString();
while (str.length < width) str = char + str;
return str;
};
var $1$exports = function () {
var exports = this;
var module = {
exports: this
};
Object.defineProperty(exports, "__esModule", {
value: true
});
var _leftpad = $3$exports;
var _leftpad2 = _interopRequireDefault(_leftpad);
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : {
default: obj
};
}
function add(a, b) {
return a + b;
}
eval('exports.foo = "bar"');
exports.default = (0, _leftpad2.default)(add(1, 2) + 4, 10);
console.log(module.exports);
return module.exports;
}.call({});
})(); Still not too bad. Already better than webpack, which doesn't support tree shaking for common js modules at all, and when eval is involved, bails out even further back to the normal module wrappers. Where this gets problematic is code splitting. We really need to be able to expose modules so they can be required from different bundles. I'm not sure yet how to do that. Maybe we can detect those modules that are required across bundles, and expose them somehow. Let me know if you have ideas on how to implement those things, and feel free to work off of this branch if you want to play around! |
This would still be possible to do if we separate the affected modules and create its own scope which would be identical to the initial bundle. I believe webpack does something similar to this. |
Yeah but in effect this is the same as not scope hoisting at all |
Totally agree, But should it matter a lot in development mode? Its better than ignoring scope hoisting for the whole development pipeline. |
Good job @devongovett! The implementation is way simpler than I originally thought. I'm currently trying to tune |
Yeah the problem is that currently we do minification on a per asset basis, but in order to get smaller sizes we'll need to do that over the entire bundle together (in the packager). Otherwise, the minifier won't be able to do dead code elimination correctly since it won't have the other assets to see what variables are used. Since this will happen across the process boundary, we probably won't be able to share the AST anyway. Happy to explore babel-minify as an alternative to uglify. Last time we compared, babel-minify was significantly slower and more buggy, but worth a try again as things may have improved. Want to try it out and compare? |
One interesting finding: minifiers can't eliminate dead babel-compiled ES modules because they have side effects by default. Input: export default function leftpad() {
return 4;
} Output: var $3$exports = {};
Object.defineProperty($3$exports, "__esModule", {
value: true
});
$3$exports.default = $3$var$leftpad;
function $3$var$leftpad() {
return 4;
} The |
Hmm, seems like uglify-es isn't even being maintained anymore actually, according to the comments here: webpack-contrib/uglifyjs-webpack-plugin#262, mishoo/UglifyJS#2908 (comment), mishoo/UglifyJS#3010 (comment). Guess it's time to look at switching to babel-minify by default. |
a.js
:b.js
:bar-module
:Build
bundle-parcel.js
(simplified)bundle-module-concat.js
using module concatenation :Minification using
babili
0.1.3 (prettified)bundle-parcel.js
:bundle-module-concat.js
:Using Prepack
bundle-parcel.js
: doesn't workbundle-module-concat.js
:Implemented by
Pro
Cons
Some questions :
The text was updated successfully, but these errors were encountered: