Replies: 5 comments 1 reply
-
@hardfist this is a great explanation of Tree Shaking and the whole process involved, thank you for putting this together! |
Beta Was this translation helpful? Give feedback.
-
Love your deep-dive articles. Keep writing <3 |
Beta Was this translation helpful? Give feedback.
-
Very insightful and digestible The examples brought the point across very clearly ❤️ |
Beta Was this translation helpful? Give feedback.
-
|
Beta Was this translation helpful? Give feedback.
-
any plans to bring barrel optimization to rspack or swc ? |
Beta Was this translation helpful? Give feedback.
-
This article primarily focuses on understanding the concept of Webpack Tree Shaking rather than delving deeply into the underlying code implementation. Code examples can be found at https://github.com/hardfist/treeshaking-cases.
One of the challenging aspects of Webpack Tree Shaking is that it involves multiple optimizations working together. Webpack's own use of the term "Tree Shaking" is somewhat inconsistent, often broadly referring to optimizations for dead code elimination. Tree Shaking is defined as:
In some contexts, optimizations like usedExports are referred to under the umbrella of tree shaking & sideEffects:
To avoid any ambiguity in understanding Tree Shaking, this discussion will not focus on Tree Shaking itself but rather on the various code optimizations under the category of Webpack Tree Shaking.
Webpack Tree Shaking primarily involves three types of optimizations:
These optimizations operate on different dimensions: usedExports focuses on export variables, sideEffects on entire modules, and DCE on JavaScript statements.
Consider the following example:
lib.js
, variableb
is unused, and related code does not appear in the final output due to usedExports optimization.util.js
, no export variables are used, resulting in the absence of theutil
module in the final output, which is a result of sideEffects optimization.bootstrap.js
, theconsole.log
statement will not execute, and thus is removed in the final output, demonstrating DCE optimization.These optimizations are implemented independently but can influence each other. Below, we detail these optimizations and their interrelationships.
DCE Optimization
DCE is relatively straightforward in Webpack, with two important scenarios:
False Branch
Here, because the
false_branch
will never execute, it can be directly removed. This has two effects: reducing the final code size and affecting the usage relationships of variables. Consider the following example:If the
false_branch
is not removed, variablea
would be considered used. Removing it marksa
as unused, which can further influence analyses for usedExports and sideEffects. To address this, Webpack offers two opportunities for DCE:Terser's DCE is more time-consuming and intricate, whereas the ConstPlugin's optimization is simpler. For example, a false branch handled by Terser can be successfully removed, but the ConstPlugin might not manage it.
Unused Top Level Statement
In modules, if a top-level statement is not exported, it can also be removed because it does not bring additional side effects. For example,
b
andtest
in the following can be safely deleted (assuming this is a module and not a script, as scripts would pollute the global scope and cannot be safely removed). Webpack's usedExports optimization leverages this characteristic to simplify its implementation.usedExports Optimization
Compared to similar optimizations by other bundlers, Webpack's usedExports optimization is quite clever. It uses the active status of dependencies to determine whether variables within a module are used. Then, during the code generation phase, if an export variable is unused, it does not generate corresponding export properties, thereby making the code segments that depend on the export variable dead code. This is further aided by subsequent minification for DCE.
Webpack enables usedExports optimization through the
optimization.usedExports
configuration. Consider the following example:Without tree shaking enabled, you can see that the output contains information about
b
:When
optimization.usedExports
is enabled, you see that the export ofb
is removed, butconst b = 2
still exists. However, sinceb
is unused,const b = 2
also becomes dead code:Further enabling compression with
optimization.usedExports
, theconst b = 2
is removed because it is dead code:However, analyzing whether
b
is used is not always straightforward. Consider the following case:Here,
b
is used by the functiontest
, so we find thatb
is not directly removed from the output. This is because Webpack does not perform deep static analysis by default. Althoughtest
is unused, implyingb
is also unused, Webpack does not deduce this relationship:Fortunately, Webpack offers another configuration,
optimization.innerGraph
, which allows for deeper static analysis of the code. This can determine thatb
is not used, thus successfully removing the export property ofb
:DCE also impacts usedExports optimization. Consider the following case:
Reliant on Webpack's internal ConstPlugin for DCE, it successfully removes
b
, but due to the limited capability of ConstPlugin, it fails to removec
.sideEffects Optimization
While usedExports optimization focuses on optimizing export variables, sideEffects optimization is more thorough and efficient, targeting the removal of entire modules. For a module to be safely removed, it must meet two conditions: none of its export variables are used, and the module must be side-effect-free.
Webpack enables sideEffects optimization through the
optimization.sideEffects
configuration. Let's look at a simple example:Without
optimization.sideEffects
enabled, the output retains theutil
module:When
optimization.sideEffects
is enabled,util.js
is removed from the output. This occurs becauseutil
meets both conditions required for removal. Let's explore what happens when we violate each condition:First, introduce side effects in
util.js
:This change causes
util.js
to reappear in the output. Now, revert that change and modifyindex.js
to use variablec
fromutil.js
:This modification also causes
util.js
to reappear in the output. These experiments demonstrate that both conditions must be met for a module to be safely removed. Ensuring these conditions are met is crucial for effectively leveraging sideEffect optimizations in practical applications.Let's revisit the two conditions necessary for the safe removal of a module:
Unused Export Variables
This condition, while seemingly straightforward, encounters similar challenges to those found in usedExports optimization and may require extensive analysis to determine how a variable is used.
Consider the following example, where
c
is used within the functiontest
, preventing the successful removal ofutil.js
:When we enable
optimization.innerGraph
, Webpack conducts a deeper analysis and determines thattest
is also unused, which implies thatc
is unused as well, allowing for the correct removal ofutil.js
.sideEffects Property
Compared to whether a variable is used, determining if a module has side effects is a more complex process. Consider the following modification to
util.js
:In this case, although the function
test
is a side-effect-free function call, Webpack is unable to determine this and still considers the module as potentially having side effects. As a result,util.js
is included in the final output.To inform Webpack that
test
has no side effects, two approaches are available:sideEffects
property to label the entire module as side-effect-free. Adding"sideEffects": false
to the module'spackage.json
allowsutil.js
to be safely removed:However, a challenge arises when a module marked as
sideEffect: false
depends on another module marked assideEffect: true
. Consider the scenario wherebutton.js
importsbutton.css
, withbutton.js
beingsideEffects: false
andbutton.css
beingsideEffects: true
:If
sideEffects
were only marking the current module for side effects, according to ESM standards, becausebutton.css
andside-effect.js
have side effects, they should be bundled. However, Webpack's output does not includebutton.css
orside-effect.js
.Therefore, the true meaning of the
sideEffects
field is:sideEffects
is much more effective since it allows to skip whole modules/files and the complete subtree. -> sideEffectIf a module is marked as
sideEffect: false
, it implies that if the module's export variables are unused, then the module and its entire subtree can be safely removed. This explanation clarifies why, in the given example, bothbutton.js
and its subtree (includingbutton.css
andside-effect.js
) can be safely deleted, which is particularly useful in the context of component libraries.Unfortunately, this behavior varies across different bundlers. Testing has shown:
Barrel Module
SideEffects optimization can optimize not only leaf node modules but also intermediate nodes. Consider a common pattern where a module re-exports the contents of other modules. If such a module itself (here referred to as
mid
) does not have any of its export variables used and only serves to re-export other modules' content, is it necessary to retain the re-export module?Testing shows that Webpack directly deletes the re-export module, and in
index.js
, it directly imports the content frombutton.js
This behavior appears as if the source code's import path was directly modified:
Frameworks like Next.js and UmiJS also offer similar optimizations Optimize Package Imports. Their approach involves rewriting these paths at the loader stage. It’s important to note that while Webpack’s barrel optimization focuses on the output, it still builds
components/index.js
and its sub-dependencies during the build phase. However, techniques used by Next.js and others modify the source code directly, meaningcomponents/index.js
does not participate in the build. This can significantly optimize libraries that re-export hundreds or thousands of sub-modules.We also tested the behavior of esbuild and Rollup regarding this:
Investigating Webpack Tree Shaking Issues
A frequent issue encountered during on-call duties is "Why has my tree shaking failed?" Troubleshooting such issues can be quite challenging. When faced with this question, the first thought is typically "Which of the tree shaking optimizations has failed?" This generally falls into one of three categories:
SideEffect Optimization Failure
The failure of sideEffect optimization is typically indicated by a module, whose export variables are not used, being included in the bundle.
A lesser-known feature of Webpack is its ability to debug various optimization bailouts through stats.optimizationBailout, including reasons for sideEffect bailouts. Consider the following example:
Compile with
optimization.sideEffects=true
andstats.optimizationBailout:true
:Webpack's logs clearly indicate that the
console.log('xxx')
on line 7 ofutil.js
caused the sideEffect optimization to fail, resulting in the module being included in the bundle.If we further configure
sideEffects: false
inpackage.json
, this warning disappears because, with the sideEffect Property set, Webpack ceases side effect analysis and directly bases sideEffect optimization on thesideEffects
field.usedExports Optimization Failure
A failure in usedExports optimization manifests when an unused export variable still generates export properties.
In such cases, it is necessary toidentify where the export properties are being used:
However, determining why and where a variable is used can be unclear, as Webpack does not provide detailed records of this. A possible improvement for Webpack could be to track and report where in the module tree specific export variables are used. This would greatly facilitate the analysis and troubleshooting of usedExports optimization issues.
DCE (Dead Code Elimination) Optimization Failure
Beyond the issues with sideEffect and usedExports optimizations, most other tree shaking failures can be attributed to failures in DCE. Common causes of DCE failure include dynamic code constructs like
eval
andnew Function
, which can lead to bailout during minification. Troubleshooting these issues typically relates to the minifier used and often requires bisecting the output code to identify the problem. Unfortunately, current minifiers seldom provide detailed reasons for bailouts, which is an area where future enhancements could be beneficial.In conclusion, effective tree shaking in Webpack requires a deep understanding of the various optimizations involved and how they interact. By correctly configuring and applying these optimizations, developers can significantly reduce the size of their bundles, enhancing performance and efficiency. As Webpack and other bundling tools evolve, ongoing learning and adjustment will be necessary to maintain optimal application performance
Beta Was this translation helpful? Give feedback.
All reactions