Webpack v4 introduced a new feature called sideEffects
, which allows you to declare in your package.json
whether a package/module contains side effects or not. This declaration provides more optimization space for tree-shaking.
In the conventional understanding of side effects, if we are certain that the modules within our package have no side effects, we can mark the package in npm
with "sideEffects": false
in package.json
. This allows us to offer a better bundling experience for consumers. The principle behind this is that Webpack can transform imports like import {a} from xx
into import {a} from 'xx/a'
for packages marked as side-effects-free, automatically trimming unnecessary imports, similar to babel-plugin-import
.
Tree Shaking and Side Effects
Tree shaking, first introduced and implemented by Rollup in the frontend community, has been a topic of discussion in various articles about optimizing bundling.
Principles of Tree Shaking
ES6 module imports are statically analyzable, meaning the compiler can accurately determine what code is loaded during compilation. The program flow is analyzed to identify unused or unreferenced variables, which are then removed from the code.
The principle sounds perfect, so why do we sometimes find that unnecessary code in our projects isn’t eliminated? The reason is side effects.
Side Effects
For those familiar with functional programming, the term “side effect” is not unfamiliar. It can be broadly understood as any action of a function that might or might not affect variables outside its scope.
For example, consider this function:
function go(url) {
window.location.href = url;
}
This function modifies the global variable location
and even triggers a browser redirect, making it a function with side effects.
// components.js
export class Person {
constructor({ name }) {
this.className = 'Person';
this.name = name;
}
getName() {
return this.name;
}
}
export class Apple {
constructor({ model }) {
this.className = 'Apple';
this.model = model;
}
getModel() {
return this.model;
}
}
// main.js
import { Apple } from './components';
const appleModel = new Apple({
model: 'IphoneX'
}).getModel();
console.log(appleModel);
In this code, the Person
class is clearly unused. However, why can other tools like Rollup successfully eliminate unused code, while Webpack cannot?
The answer lies in Babel compilation + Webpack bundling.
I’ll provide a link here that explains in detail how Babel compilation + Webpack bundling might prevent effective code elimination: Your Tree-Shaking Isn’t Working.
If you don’t want to read the article, here’s a brief explanation: Babel compilation wraps the Person
class in an IIFE (Immediately Invoked Function Expression) and returns a constructor, introducing a side effect.
There’s an issue related to this: Class declarations inside IIFEs are considered side effects.
When I declare a class inside an IIFE and don’t use the class, UglifyJS doesn’t remove it because it’s considered a side effect.
var V6Engine = (function () {
function V6Engine() {
}
V6Engine.prototype.toString = function () {
return 'V6';
};
return V6Engine;
}());
During compilation, you might receive this warning: WARN: Side effects in initialization of unused variable V6Engine [./dist/car.bundle.js:74,4]
.
The reason is that UglifyJS doesn’t perform complete program flow analysis. It doesn’t remove code because you noticed a side effect. If you want a more sophisticated tree shaking, go check out Rollup!
Summarizing some key points from the issue:
If a function’s parameter is a reference type, any operations on its properties could potentially have side effects. This is because it’s a reference type, and any modification to its properties affects data outside the function. Additionally, accessing or modifying its properties triggers
getter
orsetter
, which are opaque and may have side effects.UglifyJS lacks complete program flow analysis. It can simple judge whether a variable is later referenced or modified but cannot determine the complete modification process of a variable. It doesn’t know if it points to an external variable, so many potentially side-effect-causing code cannot be removed.
Rollup has the ability to perform program flow analysis, making it better at determining whether code truly has side effects.
However, these issues were prevalent in older versions. The current Webpack tree shaking
has undergone many optimizations and can perform sufficient program flow analysis for tree shaking
.
The purpose of Webpack’s tree shaking
is to mark unused exported members
as unused
and not export them in the modules where they are re-exported
. It sounds complicated, but looking at the code makes it clearer:
// a.js
export function a() {}
// b.js
export function b(){}
// package/index.js
import a from './a'
import b from './b'
export { a, b }
// app.js
import {a} from 'package'
console.log(a)
When using app.js
as the entry point, the code after tree shaking becomes:
// a.js
export function a() {}
// b.js is no longer exported: function b(){}
function b() {}
// package/index.js does not export module b anymore
import a from './a'
import './b'
export { a }
// app.js
import {a} from 'package'
console.log(a)
After combining Webpack’s scope hoisting
and uglify
, all traces of module b
will be completely eliminated.
But what if module b
contains some side effects, such as a simple log:
// b.js
export function b(v) { return v }
console.log(b(1))
After webpack, the content of module `b` becomes:
// b.js
console.log(function (v){return v}(1))
Although the export of module b
is ignored, the code with side effects is retained.
Due to various strange operations introduced by the transformer
after compilation, which may cause side effects, we often find that even with tree shaking, our bundle size
doesn’t significantly decrease.
Usually, we expect that if module b
is not being used, none of its code should be included.
This is where the role of sideEffects
becomes apparent: if the imported package/module is marked as "sideEffects: false"
, regardless of whether it truly has side effects, as long as it’s not being referenced, the entire module/package will be completely removed.
Taking mobx-react-devtools
as an example, we often use it like this:
import DevTools from 'mobx-react-devtools';
class MyApp extends React.Component {
render() {
return (
<div>
...
{ process.env.NODE_ENV === 'production' ? null : <DevTools /> }
</div>
);
}
}
This is a common scenario of importing modules on demand. However, without the sideEffects: false
configuration, even if NODE_ENV
is set to production
, the bundled code will still include the mobx-react-devtools
package. Although we haven’t used any of its exported members, mobx-react-devtools
will still be imported because it “might” have side effects.
But when we add sideEffects: false
, tree shaking can safely remove it entirely from the bundle.
Use Cases of sideEffects
As mentioned earlier, it’s often difficult to guarantee whether packages/modules published on npm contain side effects (it could be the code’s fault or the transformer’s fault). However, we can usually ensure whether a package/module will affect objects outside of it, such as modifying properties on the window
object or overwriting native object methods. If we can guarantee this, we can determine whether a package can have "sideEffects: false"
. Whether it truly has side effects is not that important for Webpack; it’s acceptable as long as it’s marked.
This explains why packages with inherent side effects, like vue
, can still have "sideEffects: false"
applied.
So, in Webpack, "sideEffects: false"
doesn’t mean that the module truly has no side effects. It’s just a way to tell Webpack during tree shaking: “I designed this package with the expectation that it has no side effects, even if it ends up having side effects after being bundled.”
Note: This article is a translated version of the original post. For the most accurate and up-to-date information, please refer to the original source.
```
Comments