Tree shaking in Webpack
Fogel
Posted on July 17, 2023
Introduction
Javascript file size can have a significant impact on browser performance. Unused code increases the size of our bundle file and as a result causes performance issues.
Delivering less JavaScript can mean less time in network transmission, less spent decompressing code and less time parsing and compiling this JavaScript - Addy Osmani
One effective way to reduce Javascript file size is through tree shaking.
Tree shaking is a term commonly used in the context of Javascript module bundlers. It refers to the process of removing unused code from the final bundle.
It is a powerful feature that can help you optimize the size of your Javascript bundle files. Despite its usefulness, many developers are still unfamiliar with how it works and how to use it effectively. After researching the topic and encountering some common issues, I decided to write a comprehensive guide to tree shaking in Webpack.
In this article, we'll explore how tree shaking works in Webpack and how you can use it to optimize your code. Through examples and visualizations, we'll explain the process of tree shaking step by step and help you optimize your code for better performance.
TL;DR
When building a production bundle in Webpack, the tree shaking process occurs automatically. The optimization properties of usedExports and sideEffects are enabled by default, leading to automatic optimization of the bundle files.
The main purpose of manually configuring sideEffects is to eliminate unused reexported modules within barrel files, resulting in a significant reduction in bundle size, improving performance and loading times.
Webpack
Webpack's main role is to bundle all module files into one or several files and minimize their size to save time and reduce server requests. Typically, we provide one entry point (the main Javascript file) to the Webpack config and then bundle files are created.
When executing regular Javascript (non-bundled), ES Modules evaluate every imported module file even if the imported specifier of the module is not used because the module can have side effects.
According to ECMA spec:
Do nothing if this module has already been evaluated. Otherwise, transitively evaluate all module dependences of this module and then evaluate this module
To resemble the behavior of ES Modules, Webpack includes every imported module's code (a dependency) in the bundle file without any elimination of code (except for optimization features).
Bundle Javascript project (development mode)
Let's create an example of regular Webpack bundle files without optimization.
In this example, we'll demonstrate how webpack bundles different files types (JS/CSS) into one file type main.js and main.css.
We'll use development mode because it does not have any optimizations configured by default.
To see the differences between different mode configurations (production/development/other) you can refer to the defaults.js file.
Let's take a look at the Webpack configuration:
//...
module.exports = {
mode: "development",
//...
}
Before we run the webpack command, let's take a look at our code:
import { GreenText } from "./components/Texts/GreenText/GreenText";
import { RedText } from "./components/Texts/RedText/RedText";
import { getHundredNumber } from "./tools/numberFunctions/numberFunctions";
import { getUnusedModuleText } from "./tools/unusedModule/unusedModule";
import "./tools/sideEffectsModule/sideEffectsModule";
function App() {
const hundredNumber = getHundredNumber();
return (
<>
<GreenText>Number in green: {hundredNumber}</GreenText>
<RedText>Text in red</RedText>
</>
);
}
export default App;
From the code above, we can see that we have GreenText
and RedText
components, the getHundredNumber
function and the sideEffectsModule
module in App.js
. We also import an unused function getUnusedModuleText
. These components, functions and module use different modules through direct import:
- The component
GreenText
is from the module"./components/Texts/GreenText/GreenText
. - The component
RedText
is from the module"./components/Texts/RedText/RedText"
. - The function
getHundredNumber
is from the module"./tools/numberFunctions/numberFunctions"
, which contains another two functions (getTenNumber
,getThousandNumber
). - the unused function
getUnusedModuleText
is from the module"./tools/unusedModule/unusedModule"
, which contains another function (getUnusedModuleButton
). - A side effect import module that is imported from
"./tools/sideEffectsModule/sideEffectsModule"
, which contains global code with side effects.
In the bundle process we expect that it will produce two files (JS and CSS files).
Let's run npx webpack
in the terminal window.
The build process created two files as expected: main.js
and main.css
.
Let's demonstrate it in a dependency tree:
In the main.js file, we can see that all functions and global code from all imported modules are included in the file. Even functions that weren't imported as a specifier in the import statement are included in the bundle file. As we mentioned earlier, Webpack resembles the behavior of ES Modules by importing the entire code of modules including unused code (dead code).
Even the following unused functions are included:
- The unused function
getUnusedModuleButton
from the unused module./tools/unusedModule/unusedModule
. - The unused functions
getTenNumber
andgetThousandNumber
from the used module./tools/numberFunctions/numberFunctions
.
Note: The BlueText
and BlackText
are not included because they haven't been imported anywhere.
When importing from a re-exported module, for example, a barrel file, Webpack will evaluate all re-exported modules as having side effects even if some of the re-exported modules aren't imported or used. The reason for this is written in the official Webpack example of side effects:
According to the EcmaScript spec, all child modules must be evaluated because they could contain side effects.
Sean from the Webpack team also explained this on Stack Overflow:
According to the ECMA Module Spec, whenever a module re-exports all exports, (regardless if used or unused) they need to be evaluated and executed in case one of those exports created a side-effect with another.
The used and unused functions are marked in the main.js
bundle file by the comment "harmony export"
.
These unused functions (also known as dead code) increase the size of our bundle file and cause performance issues.
To improve performance, we prefer to carefully delete unused code and make our bundle file as small as possible.
In the main.css file, we can see both class selectors of .GreenText
and .RedText
.
The term for dead code elimination is called Tree shaking and we will discuss it in the next chapter.
Tree Shaking
During the build process, Webpack creates a dependency tree structure. In the tree, each dependency (module) is represented by a branch and each function is represented by a leaf. There are green branches (which represent modules with used functions or with side effects), light green leaves (which represent imported used functions), brown leaves (which represent unused functions from imported module, also known as "dead code") and dark brown branches (which represent imported unused modules with no side effects). When we shake the tree, the brown leaves should fall off while the green leaves remain. In our case, this means that after tree shaking only imported used functions or modules with used functions or side effects should remain.
As we saw in the last example, used and unused functions were marked the same with the comment "harmony export"
that specifies the green leaves.
How can it be that unused functions were marked as green leaves? Because we were in development mode, which has no optimization enabled.
So how can we mark the unused functions as brown leaves and unused modules without side effects as dark brown branches? For this, we should use the optimization configuration.
Optimization
usedExports
The usedExports
property is responsible for marking the unused functions with brown leaves.
Let's clone the previous example 1-without-optimization
to 2-optimization-usedExports
and change the Webpack config file as follows:
//...
module.exports = {
mode: "development",
+ optimization: {
+ usedExports: true,
+ },
//...
}
Now run npx webpack
in the terminal window.
Let's demonstrate it in a dependency tree:
Take a look at the bundle file:
//...
/* unused harmony exports getTenNumber, getThousandNumber */
function getTenNumber() {
return 10;
}
function getHundredNumber() {
return 100;
}
function getThousandNumber() {
return 1000;
}
//...
/* unused harmony exports getUnusedModuleText, getUnusedModuleButton */
function getUnusedModuleText() {
return "imported but unused module";
}
function getUnusedModuleButton() {
return "imported but unused module";
}
In the main.js file we can see that Webpack with the usedExports: true
property marked the unused functions (dead code) with the comment /* unused harmony export functionName */
. This comment points to the unused function as a brown leaf in the tree.
But the entire unused function declarations are still in the file. How can we get rid of it? How can we shake the tree and cause brown leaves to fall down (eliminate dead code) to the ground? We will shake the tree later using either the configuration of minimize: true
or mode: production
.
In the main.css file, we can see that it's the same as in the first example 1-without-optimization
.
sideEffects
First, let's explain what side effects are.
There are two types of code: pure code and code with side effects:
-
Pure code: It uses only variables from its scope and doesn't use global variables like the
window
object or data that comes from the outside sources such as HTTP requests, file system, DOM, etc. For example, a pure function always returns the same output for the same input and doesn't use global variables. In short, pure code is a code without side effects. -
Code with side effects: It uses global variables or data from outside its scope. For example, a function that can return different output for the same input or a function that changes global variables like the
window
object or uses data from HTTP calls, file system, DOM, etc. In short, It is the opposite of pure code.
In Webpack, sideEffects
configuration enables us to exclude entire module (and his dependencies) whose direct exports are not used and doesn't have side effects from the bundle file.
Point to remember to this section: In Webpack, the sideEffects
configuration only affects imported modules whose direct exports are not used. Modules with used exports will always be included. An example of imported modules whose direct exports are not used are barrel files that re-export functions.
There are two configurations for sideEffects
that are designed to work together if they configured:
- The
sideEffects
property in theoptimization
object inwebpack.config.js
- Enable the use for manual configuration of modules with or without side effects and/or automatic analysis of modules for side effects. - The manual
sideEffects
property inside eitherpackage.json
file or themodule.rules
array in thewebpack.config.js
file - Manually configure the list of modules that have or don't have side effects. In this case modules whose direct exports are not used flagged manually as having no side effects, then these modules and their dependencies will be safely excluded from the bundle file without even being evaluated.
sideEffects optimization
According to WebpackOptions.json, the optimization sideEffects
property can have the following values:
-
true
- Include or exclude modules depending on their manual configuration in thepackage.json
orwebpack.config.js
file. Modules that haven't been flagged with or without side effects in thewebpack.config.js
file and whose exports are not used will be analyzed, if there are no side effects, they will be excluded. -
"flag"
- Only include or exclude modules depending on their manual configuration in thepackage.json
orwebpack.config.js
file. Modules that haven't been flagged with or without side effects in thewebpack.config.js
file will be included as usual, just like we learned about ES modules. -
false
- It will not evaluate modules for side effects even if set in thepackage.json
orwebpack.config.js
file. All imported modules will be included as we learned about ES modules.
Note
If the modules with side effects are flagged in the package.json
file, the remaining files will be flagged as having no side effects. So if modules are configured through the package.json
file, there will be no difference between the values true
and "flag"
. The result of them will be the same because all modules are flagged with or without side effects and there is no module that isn't flagged. Then the true
value will not have modules to analyze because all modules have been flagged with or without side effects, so true
and "flag"
will have the same behavior.
Production mode uses the optimization sideEffects
value of true
.
Before we dive into the manual way of configuring sideEffects, let's make an example about the optimization sideEffects
property without manually defining which modules have or don't have side effects:
Clone the previous example 2-optimization-usedExports
to 3-optimization-sideEffects
and change the webpack config file as follows:
//...
module.exports = {
mode: "development",
optimization: {
usedExports: true,
+ sideEffects: true,
},
//...
}
Now run npx webpack
in the terminal window.
Let's demonstrate it in a dependency tree:
Take a look at the bundle file:
//...
/* unused harmony exports getTenNumber, getThousandNumber */
function getTenNumber() {
return 10;
}
function getHundredNumber() {
return 100;
}
function getThousandNumber() {
return 1000;
}
//...
In the main.js file, we can see that Webpack with the sideEffects: true
property eliminates the unused module of unusedModule
(dead code) compared to the previous example 2-optimization-usedExports
which just marked unusedModule
with /* unused harmony export getUnusedModuleText, getUnusedModuleButton */
. When we use in mode: production
, which under the hood uses minimize: true
, for both the second and third examples, both bundle files will be the same because the unused unusedModule
will be eliminated during the minimizing process.
In the main.css file, we can see that it's the same as in the first example 1-without-optimization
.
sideEffects - Manually define
Now, if we set the optimization sideEffects
property to true
or "flag"
, we can help Webpack manually define which modules have side effects and which don't.
Sean from the Webpack team explains on Stack Overflow that we use the sideEffects
configuration as a way to save on both compile time and build size because it excludes modules in advance that are flagged as having no side effects and whose direct exports are not used.
According to Webpack documentation:
sideEffects
is much more effective since it allows to skip whole modules/files and the complete subtree.
usedExports
relies on Terser to detect side effects in statements. It is a difficult task in JavaScript and not as effective as straightforwardsideEffects
flag.
Webpack's team talked about another purpose of the manual sideEffects
property in their conference and explained that it comes to solve the issue of big bundle files when using of barrel files.
There are 2 ways to manually define which modules have side effects and which don't:
-
Package config file (
package.json
):All package modules have no side effects:
{ "name": "example-module", "sideEffects": false }
Alternatively, an array of modules that have side effects (the remaining modules will be considered and flagged as having no side effects if they don't have used direct exports):
{ "name": "example-module", "sideEffects": ["*.css", "*.scss"] }
-
Webpack config file (
webpack.config.js
):Use regular expressions to define all Javascript and Typescript files in the project as having no side effects and CSS files as having side effects:
module: { rules: [ { test: /\.(js|mjs|jsx|ts|tsx)$/, sideEffects: false }, { test: /\.s?css$/, sideEffects: true } ] }
We know that CSS files are considered to have side effects, so we flag them as such. However, this is not mandatory and we can remove the rule for CSS files above. The result will be the same, it's just for protection. if someone later configures the package.json
file with sideEffects: false
, the rule in webpack.config.js
for CSS will override the configuration in package.json
Webpack wrote a "Tip" about CSS files as described on Webpack website:
> Note that any imported file is subject to tree shaking. This means if you use something like css-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.
Side effect import - import "filename.js"
In Side effect import also called "Empty import", It will need to be included if there are side effects inside it. This can include, for example, global code and CSS files.
import "filename.js";
import "filename.css";
In our examples, we use a module with side effects called sideEffectsModule
. Note that this module has no exports but have side effects cause it affects window
object. So this kind of module can easily be excluded from the bundle file if it is configured incorrectly and not included in the array of the sideEffects
property in the package.json
file.
So let's make a manual configuration of sideEffects
in the package.json
file.
Let's clone the previous example 3-optimization-sideEffects
to 4-package-sideEffects
and change the package config file as follows:
{
"name": "example-module",
+ "sideEffects": ["*.css"],
}
Alternatively:
webpack.config.js
module: {
rules: [
{
test: /\.(js|mjs|jsx|ts|tsx)$/,
+ sideEffects: false
}
]
}
In our example we use only the package.json
option, but both options have the same result.
Now run npx webpack
in the terminal window.
Let's demonstrate it in a dependency tree:
In the main.js file, we can see that it is similar to the previous example (3-optimization-sideEffects) except that the content of sideEffectsModule
is not included. This occurred because sideEffectsModule
doesn't have any exports and was flagged as having no side effects. As a result, Webpack doesn't evaluate it for side effects and simply skips over the sideEffectsModule
module.
In the main.css file, we can see that it's the same as in the first example (1-without-optimization).
So what are the differences between the third and fourth examples except the missing global code of sideEffectsModule
module? It's about compile time, as we talked earlier about Sean from the Webpack team. In the fourth example, Webpack skips over modules with unused exports that have been flagged as having no side effects without attempting to evalute them for side effects.
sideEffects - Manually define fixed
Let's make the same example but this time we will correctly configure the package.json
file.
{
"name": "example-module",
- "sideEffects": ["*.css"],
+ "sideEffects": ["*.css", "sideEffectsModule.js"],
}
Now run npx webpack
in the terminal window.
Let's demonstrate it in a dependency tree:
In the main.js file, we can see that now it fixed and sideEffectsModule
module content is included in the bundle file.
Note - Manually configuring sideEffects issue
I found an issue where Hypnosphi complained that there were differences between development and production when he configured sideEffects: false
. He noticed that the order of CSS was not the order of the imports but the usage order. In another issue there is an answer from sokra (a Webpack team member) who wrote:
Technically using sideEffects you say order doesn't matter
Another answer of sokra:
If the order of some modules matter they are not side-effect-free.
I will expand on this in the article Potential issues with barrel files in Webpack talking about issues with optimization configuration and how to solve them.
If I only import the modules that I use, why should I use the sideEffects
property? It's useful for modules that only re-exports functions from other modules. For example, barrel files that are used inside modules that are dynamically imported. I will expand on this in my next article.
Summarize the sideEffects
configuration in a diagram:
Production mode
The last thing that remains to check is the production bundle.
Production mode has different optimization values compared to development mode. We can see in the default.js file that minimize
is set to true.
The minimize
property is used as the final tree shaking step. It eliminates unused exports (brown leaves) that were marked by usedExports
.
Let's clone the third example 3-optimization-sideEffects
to 6 - production
and change the webpack config file as follows:
//...
module.exports = {
+ mode: "production",
- mode: "development",
- optimization: {
- usedExports: true,
- sideEffects: true,
- },
//...
}
Now run npx webpack
in the terminal window.
Let's demonstrate it in a dependency tree:
In the main.js file, we can see that it's significantly smaller file. It only includes what's needed and mangles the variable and function names.
In the main.css file, we can see that it has changed here because it eliminated comments in the file compared to previous examples.
After optimizing and minifying our bundle file, let's take a look at the results.
As shown in the screenshot, the size of the JS files has decreased significantly - from 1.36kb to 626 bytes, a reduction of 54%. This is a substantial decrease in size.
Tree shaking summary steps
- Eliminating imported modules whose direct exports are not used and don't have side effects with
sideEffects
configuration. - Mark the brown leaves (comment:
/* unused harmony export functionName */
) with theusedExports: true
property. - Shake the tree (eliminate dead code) with the
minimize: true
ormode: production
property.
Dynamic imports
This article will not cover the topic of tree shaking dynamic imports. For an in-depth explanation of this subject, please refer to the awesome article Dynamically Import ESModules and Tree Shake them too!.
Summary
In this article, we explored the concept of tree shaking in Webpack to optimize your bundle file. We explained how tree shaking can be used to remove unused functions and modules (dead code) from the final bundle and which optimization properties involved in that through examples and visualizations. The main purpose of this article was to ease the tree shaking subject and I hope that I succeeded in that.
I recommend reading my next article on Potential issues with barrel files in Webpack.
Additional Resources:
Reduce JavaScript payloads with tree shaking
Tree Shaking in official Webpack website
Posted on July 17, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.