🌳 Scope Hoisting

What scope hoisting is and how it enables smaller builds and ESM output

Parcel can remove unused JS code with both CommonJS and ES modules (including dynamic imports in many cases), and unused CSS modules classes.

ΒΆ Tips for smaller/faster builds

ΒΆ Wrapped Assets

There are a few cases where an asset needs to be wrapped, that is moved inside a function. This negates some advantages of scope-hoisting.

ΒΆ sideEffects: false

When sideEffects: false is specified in package.json (in most cases of some library), Parcel can skip processing some assets entirely (e.g. not even transpiling the lodash function that weren't imported) or not include them in the output bundle at all (e.g. because that asset merely does reexporting).

ΒΆ import * as ns from "...";

Even if you use the import * as syntax, unused exports are removed reliably as long as the namespace object is only accessed with static member expressions (ns.foo or ns['foo']).

 
import * as thing from "./foo.js";

console.log(thing.x);

let other = thing; // This causes everything to be included!
console.log(other.x);

ΒΆ Motivation and Advantages of Scope Hoisting

For a long time, many bundlers (like Webpack and Browserify, but not Rollup) achieved the actual bundling by wrapping all assets in a function, creating a map of all included assets and providing a CommonJS runtime. A (very) simplified example of that:

(function (modulesMap, entry) {
// internal runtime
})(
{
"index.js": function (require, module, exports) {
var { Foo } = require("./thing.js");
var obj = new Foo();
obj.run();
},
"thing.js": function (require, module, exports) {
module.exports.Foo = class Foo {
run() {
console.log("Hello!");
}
};
module.exports.Bar = class Bar {
run() {
console.log("Unused!");
}
};
},
},
"index.js"
);

This mechanism has both advantages and disadvantages:

ΒΆ Solution

Instead, the individual assets are concatenated directly in the top-level scope:

// thing.js
var $thing$export$Foo = class {
run() {
console.log("Hello!");
}
};
var $thing$export$Bar = class {
run() {
console.log("Unused!");
}
};

// index.js
var $index$export$var$obj = new $thing$export$Foo();
$index$export$var$obj.run();

As you can see, the top-level variables from the assets need to be renamed to have a globally unique name.

Now, removing unused exports has become trivial: the variable $thing$export$Bar is not used at all, so we can safely remove it (and a minifier like Terser would do this automatically), this step is referred to as tree shaking.

The only real downside is that builds take quite a bit longer and also use more memory than the wrapper-based approach (because every single statement needs to be modified and the bundle as a whole needs to remain in memory during the packaging).