Movatterモバイル変換


[0]ホーム

URL:


webpack logo
ag grid
ag charts

Tree Shaking

Tree shaking is a term commonly used in the JavaScript context for dead-code elimination. It relies on thestatic structure of ES2015 module syntax, i.e.import andexport. The name and concept have been popularized by the ES2015 module bundlerrollup.

The webpack 2 release came with built-in support for ES2015 modules (aliasharmony modules) as well as unused module export detection. The new webpack 4 release expands on this capability with a way to provide hints to the compiler via the"sideEffects"package.json property to denote which files in your project are "pure" and therefore safe to prune if unused.

tip

The remainder of this guide will stem fromGetting Started. If you haven't read through that guide already, please do so now.

Add a Utility

Let's add a new utility file to our project,src/math.js, that exports two functions:

project

webpack-demo|- package.json|- package-lock.json|- webpack.config.js|- /dist |- bundle.js |- index.html|- /src |- index.js+ |- math.js|- /node_modules

src/math.js

exportfunctionsquare(x){return x* x;}exportfunctioncube(x){return x* x* x;}

Set themode configuration option todevelopment to make sure that the bundle is not minified:

webpack.config.js

const path = require('path');module.exports = { entry: './src/index.js', output: {   filename: 'bundle.js',   path: path.resolve(__dirname, 'dist'), },+ mode: 'development',+ optimization: {+   usedExports: true,+ },};

With that in place, let's update our entry script to utilize one of these new methods and removelodash for simplicity:

src/index.js

- import _ from 'lodash';+ import { cube } from './math.js'; function component() {-   const element = document.createElement('div');+   const element = document.createElement('pre');-   // Lodash, now imported by this script-   element.innerHTML = _.join(['Hello', 'webpack'], ' ');+   element.innerHTML = [+     'Hello webpack!',+     '5 cubed is equal to ' + cube(5)+   ].join('\n\n');   return element; } document.body.appendChild(component());

Note that wedid notimport thesquare method from thesrc/math.js module. That function is what's known as "dead code", meaning an unusedexport that should be dropped. Now let's run our npm script,npm run build, and inspect the output bundle:

dist/bundle.js (around lines 90 - 100)

/* 1 *//***/(function(module, __webpack_exports__, __webpack_require__){'use strict';/* unused harmony export square *//* harmony export (immutable) */ __webpack_exports__['a']= cube;functionsquare(x){return x* x;}functioncube(x){return x* x* x;}});

Note theunused harmony export square comment above. If you look at the code below it, you'll notice thatsquare is not being imported, however, it is still included in the bundle. We'll fix that in the next section.

Mark the file as side-effect-free

In a 100% ESM module world, identifying side effects is straightforward. However, we aren't there quite yet, so in the mean time it's necessary to provide hints to webpack's compiler on the "pureness" of your code.

The way this is accomplished is the"sideEffects" package.json property.

{"name":"your-project","sideEffects":false}

All the code noted above does not contain side effects, so we can mark the property asfalse to inform webpack that it can safely prune unused exports.

tip

A "side effect" is defined as code that performs a special behavior when imported, other than exposing one or more exports. An example of this are polyfills, which affect the global scope and usually do not provide an export.

If your code did have some side effects though, an array can be provided instead:

{"name":"your-project","sideEffects":["./src/some-side-effectful-file.js"]}

The array accepts simple glob patterns to the relevant files. It usesglob-to-regexp under the hood (Supports:*,**,{a,b},[a-z]). Patterns like*.css, which do not include a/, will be treated like**/*.css.

tip

Note that any imported file is subject to tree shaking. This means if you use something likecss-loader in your project and import a CSS file, it needs to be added to the side effect list so it will not be unintentionally dropped in production mode:

{"name":"your-project","sideEffects":["./src/some-side-effectful-file.js","*.css"]}

Finally,"sideEffects" can also be set from themodule.rules configuration option.

Clarifying tree shaking andsideEffects

ThesideEffects andusedExports (more known as tree shaking) optimizations are two different things.

sideEffects is much more effective since it allows to skip whole modules/files and the complete subtree.

usedExports relies onterser to detect side effects in statements. It is a difficult task in JavaScript and not as effective as straightforwardsideEffects flag. It also can't skip subtree/dependencies since the spec says that side effects need to be evaluated. While exporting function works fine, React's Higher Order Components (HOC) are problematic in this regard.

Let's make an example:

import{ Button}from'@shopify/polaris';

The pre-bundled version looks like this:

import hoistStaticsfrom'hoist-non-react-statics';functionButton(_ref){// ...}functionmerge(){var _final={};for(var _len= arguments.length, objs=newArray(_len), _key=0;    _key< _len;    _key++){    objs[_key]= arguments[_key];}for(var _i=0, _objs= objs; _i< _objs.length; _i++){var obj= _objs[_i];mergeRecursively(_final, obj);}return _final;}functionwithAppProvider(){returnfunctionaddProvider(WrappedComponent){var WithProvider=/*#__PURE__*/(function(_React$Component){// ...return WithProvider;})(Component);    WithProvider.contextTypes= WrappedComponent.contextTypes?merge(WrappedComponent.contextTypes, polarisAppProviderContextTypes): polarisAppProviderContextTypes;var FinalComponent=hoistStatics(WithProvider, WrappedComponent);return FinalComponent;};}var Button$1=withAppProvider()(Button);export{// ...,  Button$1,};

WhenButton is unused you can effectively remove theexport { Button$1 }; which leaves all the remaining code. So the question is "Does this code have any side effects or can it be safely removed?". Difficult to say, especially because of this linewithAppProvider()(Button).withAppProvider is called and the return value is also called. Are there any side effects when callingmerge orhoistStatics? Are there side effects when assigningWithProvider.contextTypes (Setter?) or when readingWrappedComponent.contextTypes (Getter?).

Terser actually tries to figure it out, but it doesn't know for sure in many cases. This doesn't mean that terser is not doing its job well because it can't figure it out. It's too difficult to determine it reliably in a dynamic language like JavaScript.

But we can help terser by using the/*#__PURE__*/ annotation. It flags a statement as side effect free. So a small change would make it possible to tree-shake the code:

var Button$1=/*#__PURE__*/withAppProvider()(Button);

This would allow to remove this piece of code. But there are still questions with the imports which need to be included/evaluated because they could contain side effects.

To tackle this, we use the"sideEffects" property inpackage.json.

It's similar to/*#__PURE__*/ but on a module level instead of a statement level. It says ("sideEffects" property): "If no direct export from a module flagged with no-sideEffects is used, the bundler can skip evaluating the module for side effects.".

In the Shopify's Polaris example, original modules look like this:

index.js

import'./configure';export*from'./types';export*from'./components';

components/index.js

// ...export{defaultas Breadcrumbs}from'./Breadcrumbs';export{defaultas Button, buttonFrom, buttonsFrom}from'./Button';export{defaultas ButtonGroup}from'./ButtonGroup';// ...

package.json

// ..."sideEffects":["**/*.css","**/*.scss","./esnext/index.js","./esnext/configure.js"],// ...

Forimport { Button } from "@shopify/polaris"; this has the following implications:

  • include it: include the module, evaluate it and continue analysing dependencies
  • skip over: don't include it, don't evaluate it but continue analysing dependencies
  • exclude it: don't include it, don't evaluate it and don't analyse dependencies

Specifically per matching resource(s):

  • index.js: No direct export is used, but flagged with sideEffects -> include it
  • configure.js: No export is used, but flagged with sideEffects -> include it
  • types/index.js: No export is used, not flagged with sideEffects -> exclude it
  • components/index.js: No direct export is used, not flagged with sideEffects, but reexported exports are used -> skip over
  • components/Breadcrumbs.js: No export is used, not flagged with sideEffects -> exclude it. This also excluded all dependencies likecomponents/Breadcrumbs.css even if they are flagged with sideEffects.
  • components/Button.js: Direct export is used, not flagged with sideEffects -> include it
  • components/Button.css: No export is used, but flagged with sideEffects -> include it

In this case only 4 modules are included into the bundle:

  • index.js: pretty much empty
  • configure.js
  • components/Button.js
  • components/Button.css

After this optimization, other optimizations can still apply. For example:buttonFrom andbuttonsFrom exports fromButton.js are unused too.usedExports optimization will pick it up and terser may be able to drop some statements from the module.

Module Concatenation also applies. So that these 4 modules plus the entry module (and probably more dependencies) can be concatenated.index.js has no code generated in the end.

Full Example: Understanding Side Effects with CSS Files

To better understand the impact of thesideEffects flag, let's look at a complete example of an npm package with CSS assets and how they might be affected during tree shaking. We'll create a fictional UI component library called "awesome-ui".

Package Structure

Our example package looks like this:

awesome-ui/├── package.json├── dist/│   ├── index.js│   ├── components/│   │   ├── index.js│   │   ├── Button/│   │   │   ├── index.js│   │   │   └── Button.css│   │   ├── Card/│   │   │   ├── index.js│   │   │   └── Card.css│   │   └── Modal/│   │       ├── index.js│   │       └── Modal.css│   └── theme/│       ├── index.js│       └── defaultTheme.css

Package Files Content

package.json

{"name":"awesome-ui","version":"1.0.0","main":"dist/index.js","sideEffects":false}

dist/index.js

export*from'./components';export*from'./theme';

dist/components/index.js

export{defaultas Button}from'./Button';export{defaultas Card}from'./Card';export{defaultas Modal}from'./Modal';

dist/components/Button/index.js

import'./Button.css';// This has a side effect - it applies styles when imported!exportdefaultfunctionButton(props){// Button component implementationreturn{    type:'button',...props,};}

dist/components/Button/Button.css

.awesome-ui-button{background-color: #0078d7;color: white;padding: 8px 16px;border-radius: 4px;border: none;cursor: pointer;}

dist/components/Card/index.js anddist/components/Modal/index.js would have similar structure.

dist/theme/index.js

import'./defaultTheme.css';// This has a side effect!exportconst themeColors={  primary:'#0078d7',  secondary:'#f3f2f1',  danger:'#d13438',};

What Happens When Consuming This Package?

Now, imagine a consumer application that only wants to use the Button component:

import{ Button}from'awesome-ui';// Use the Button component

WithsideEffects: false in package.json

When webpack processes this import with tree shaking enabled:

  1. It sees the import for only Button
  2. It looks at the package.json and seessideEffects: false
  3. It determines it only needs to include the Button component code
  4. Since all files are marked as having no side effects, it will includeonly the JavaScript code for the Button
  5. The CSS file import gets dropped! Even though Button.css is imported in Button/index.js, webpack assumes this import has no side effects.

The result: The Button component will render, but without any styling since Button.css was eliminated during tree shaking.

The Correct Configuration for This Package

To fix this, we need to update package.json to properly mark CSS files as having side effects:

{"name":"awesome-ui","version":"1.0.0","main":"dist/index.js","sideEffects":["**/*.css"]}

With this configuration:

  1. Webpack still identifies that only the Button component is needed
  2. But now it recognizes that CSS files have side effects
  3. So, it includes Button.css when processing Button/index.js

The Decision Tree for Side Effects

Here's how webpack evaluates modules during tree shaking:

  1. Is the export from this module used directly or indirectly?

    • If yes: Include the module
    • If no: Continue to step 2
  2. Is the module marked with side effects?

    • If yes (sideEffects includes this file or istrue): Include the module
    • If no (sideEffects isfalse or doesn't include this file): Exclude the module and its dependencies

For our library's files with the proper sideEffects configuration:

  • dist/index.js: No direct export used, no side effects -> Skip over
  • dist/components/index.js: No direct export used, no side effects -> Skip over
  • dist/components/Button/index.js: Direct export used -> Include
  • dist/components/Button/Button.css: No exports, has side effects -> Include
  • dist/components/Card/*: No exports used, no side effects -> Exclude
  • dist/components/Modal/*: No exports used, no side effects -> Exclude
  • dist/theme/*: No exports used, no side effects -> Exclude

Real-World Impact

The impact of incorrect side effects configuration can be significant:

  1. CSS not being included: Components render without styles
  2. Global JavaScript not running: Polyfills or global configurations don't execute
  3. Initialization code skipped: Functions that register components or set up event listeners never run

These issues can be particularly hard to debug because they often only appear in production builds when tree shaking is enabled.

Testing Side Effects Configuration

A good way to test if your side effects configuration is correct:

  1. Create a minimal application that imports just one component
  2. Build it with production settings (with tree shaking enabled)
  3. Check if all necessary styles and behaviors work correctly
  4. Look at the generated bundle to confirm the right files are included

Mark a function call as side-effect-free

It is possible to tell webpack that a function call is side-effect-free (pure) by using the/*#__PURE__*/ annotation. It can be put in front of function calls to mark them as side-effect-free. Arguments passed to the function are not being marked by the annotation and may need to be marked individually. When the initial value in a variable declaration of an unused variable is considered as side-effect-free (pure), it is getting marked as dead code, not executed and dropped by the minimizer.This behavior is enabled whenoptimization.innerGraph is set totrue.

file.js

/*#__PURE__*/double(55);

Minify the Output

So we've cued up our "dead code" to be dropped by using theimport andexport syntax, but we still need to drop it from the bundle. To do that, set themode configuration option toproduction.

webpack.config.js

const path = require('path');module.exports = { entry: './src/index.js', output: {   filename: 'bundle.js',   path: path.resolve(__dirname, 'dist'), },- mode: 'development',- optimization: {-   usedExports: true,- }+ mode: 'production',};
tip

Note that the--optimize-minimize flag can be used to enableTerserPlugin as well.

With that squared away, we can run anothernpm run build and see if anything has changed.

Notice anything different aboutdist/bundle.js? The whole bundle is now minified and mangled, but, if you look carefully, you won't see thesquare function included but will see a mangled version of thecube function (function r(e){return e*e*e}n.a=r). With minification and tree shaking, our bundle is now a few bytes smaller! While that may not seem like much in this contrived example, tree shaking can yield a significant decrease in bundle size when working on larger applications with complex dependency trees.

tip

ModuleConcatenationPlugin is needed for the tree shaking to work. It is added bymode: 'production'. If you are not using it, remember to add theModuleConcatenationPlugin manually.

Common Pitfalls with Side Effects

When working with tree shaking and thesideEffects flag, there are several common pitfalls to avoid:

1. Over-optimisticsideEffects: false

SettingsideEffects: false in your package.json is tempting for optimal tree shaking, but this can cause problems if your code actually does have side effects. Examples of hidden side effects:

  • CSS imports (as demonstrated above)
  • Polyfills that modify global objects
  • Libraries that register global event listeners
  • Code that modifies prototype chains

2. Re-exports with Side Effects

Consider this pattern:

// This file has side effects that might be skippedimport'./polyfill';// Re-export componentsexport*from'./components';

If a consumer only imports specific components, the polyfill import might be skipped entirely if not properly marked with side effects.

3. Forgetting about Nested Dependencies

Your package might correctly mark side effects, but if it depends on third-party packages that incorrectly mark their side effects, you might still encounter issues.

4. Testing Only in Development Mode

Tree shaking typically only fully activates in production mode. Testing only in development can hide tree shaking issues until deployment.

Conclusion

What we've learned is that in order to take advantage oftree shaking, you must...

  • Use ES2015 module syntax (i.e.import andexport).
  • Ensure no compilers transform your ES2015 module syntax into CommonJS modules (this is the default behavior of the popular Babel preset @babel/preset-env - see thedocumentation for more details).
  • Add a"sideEffects" property to your project'spackage.json file.
  • Be careful about correctly marking files with side effects, especially CSS imports.
  • Use theproductionmode configuration option to enablevarious optimizations including minification and tree shaking (side effects optimization is enabled in development mode using the flag value).
  • Make sure you set a correct value fordevtool as some of them can't be used inproduction mode.

You can imagine your application as a tree. The source code and libraries you actually use represent the green, living leaves of the tree. Dead code represents the brown, dead leaves of the tree that are consumed by autumn. In order to get rid of the dead leaves, you have to shake the tree, causing them to fall.

If you are interested in more ways to optimize your output, please jump to the next guide for details on building forproduction.

17 Contributors

simon04zacangeralexjovermavant1dmitriidprobablyupgishlumo10byzykpnevaresEugeneHlushkoAnayaDesigntorifatrahul3vsnitin315vansh5632

[8]ページ先頭

©2009-2025 Movatter.jp