If you have any thoughts on my blog or articles and you want to let me know, you can either post a comment below(public) or tell me via this i_kkkp@163.com

How to Use `sideEffects` in Webpack

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 or setter, 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.
```

301/302 Redirection Webpack Performance Optimization-2

Comments