Angular 98 🅰️ Tree Shaking and Bundle Analysis
A production build is not just a minified version of the development build. It is a different artifact. The bundler removes code that is never used, renames variables to shorter names, and splits the application into chunks. The process that removes the unused code is called tree shaking. The process that finds what is left is called bundle analysis. Together, they are the tools that determine whether the deployed application is lean or bloated.
The Angular CLI performs tree shaking automatically in production builds. The bundler starts from the entry point — main.ts — and crawls every import. Any module or export that is not reachable from the entry point is never included in the build. The buildOptimizer plugin goes further: it adds /*@__PURE__*/ annotations to Angular decorators, which tells the minifier that the decorated classes have no side effects and can be removed if they are not referenced elsewhere . The result is a bundle that contains only the code the application actually uses.
Key point: Tree shaking removes unused code. Bundle analysis shows what remains. A production build that is not analyzed is a build that might be carrying hundreds of kilobytes of code that the application never executes. The bundle analyzer renders the build as a treemap, and the large blocks in the treemap are the optimization opportunities.
Why tree shaking matters
A modern Angular application depends on dozens of packages. RxJS, Angular Material, date libraries, utility libraries, and the Angular framework itself. Each package contains more code than the application uses. Tree shaking is what separates the code the application needs from the code it does not.
The unused export problem. A utility library exports fifty functions. The application uses three. Without tree shaking, all fifty functions are included in the bundle. With tree shaking, only the three are included. The difference is measured in kilobytes per library.
The side-effect problem. Tree shaking can only remove code that has no side effects. A module with top-level code that modifies global state — a polyfill, a CSS import, a console.log — cannot be removed even if its exports are unused . The sideEffects field in package.json tells the bundler which files have side effects and which do not. A library that marks itself as sideEffects: false allows the bundler to remove every unused export .
The injection token problem. Angular’s dependency injection system uses classes as tokens. When a class is used as a token in a @ContentChild decorator or a constructor parameter, the reference is in the value position. The compiler must keep the reference at runtime, which prevents the class from being tree-shaken . The lightweight injection token pattern solves this by using a small abstract class as the token and providing the actual implementation later .
The build optimizer problem. Angular decorators compile to function calls. The minifier cannot tell whether a decorated class has side effects. The buildOptimizer plugin adds /*@__PURE__*/ annotations to the compiled decorator calls, which tells the minifier that the class is safe to remove if it is not referenced .
The trade-off. Tree shaking requires discipline from both application developers and library authors. The application must use named imports instead of namespace imports. The library must declare its side effects correctly. The decorators must be marked as pure. When all of this is done, the bundle is small. When any of it is missing, the bundle carries dead code.
a. How Tree Shaking Works
Tree shaking is dead-code elimination. The term was popularized by Rollup and adopted by webpack and esbuild . The concept is simple: if a piece of code is never used, and removing it does not change the behavior of the program, remove it.
The bundler builds a dependency graph starting from the entry point. Every import statement adds an edge to the graph. Every export is a node. The bundler marks the exports that are reachable from the entry point. The exports that are not marked are removed.
The static structure of ES modules makes this possible. An import statement is static: the bundler can see it at build time. A require call is dynamic: the bundler cannot always see it. This is why ES modules are tree-shakeable and CommonJS modules are not .
The bundler also performs side-effect analysis. A module with top-level code that runs on import has side effects. A module that only defines functions and classes does not. The sideEffects field in package.json declares the side effects. If the field is false, the bundler assumes every file in the package is side-effect-free and removes unused files. If the field is an array, the bundler preserves the files that match the patterns and removes the rest .
The /*@__PURE__*/ annotation is the escape hatch. It tells the bundler that a function call or new expression has no side effects. If the result is unused, the entire call can be removed . The Angular buildOptimizer adds this annotation to the compiled Angular decorators, which is what makes Angular classes tree-shakeable .
b. Bundle Analysis
A bundle analyzer reads the build’s statistics file and renders the bundle as a treemap. The treemap shows every module in the bundle, the size of each module, and the chunks they belong to. The large blocks in the treemap are the optimization opportunities.
The Angular CLI produces the statistics file when the build is run with the --stats-json flag:
ng build --configuration production --stats-json
The stats.json file is written to the build output directory. The file contains the esbuild metafile, which describes every module, every chunk, and every import edge .
The webpack-bundle-analyzer tool renders the statistics file as an interactive treemap:
npx webpack-bundle-analyzer dist/my-app/stats.json
The tool opens a browser window with the treemap. The treemap is organized by chunk. The initial chunk is the code the browser downloads before the application boots. The lazy chunks are the code that is downloaded on demand. The treemap shows the size of each module within each chunk .
The angular-esbuild-visualizer tool is designed specifically for Angular’s two-tier loading model. It distinguishes between the initial download and the lazy routes. It answers the question that the generic analyzers do not: which chain of imports pulled a heavy module into the initial download, and which edge can be cut to remove it .
The analysis produces a set of decisions. A large module that is not needed on the initial route can be lazy-loaded. A library that is larger than its value can be replaced with a lighter alternative. A module that is duplicated across chunks can be extracted into a shared chunk. A module that is not used at all can be removed.
c. What to Look For
The bundle analyzer shows the size of every module. The goal is to find the modules that are large and unnecessary.
Large libraries. A date library like Moment.js is over 200 KB and is not tree-shakeable. A lighter alternative like date-fns is tree-shakeable and adds only the functions that are used . A UI library that includes every component in the initial bundle can be replaced with the CDK and custom components, or with lazy-loaded feature modules .
Namespace imports. An import * as utils from './utils' imports everything from the module. Even if the application uses only one function, the bundler cannot remove the rest because the namespace object is a runtime value. Named imports — import { simpleSort } from './utils' — allow the bundler to remove the unused exports .
Side-effectful modules. A module with a top-level console.log or a global variable assignment cannot be tree-shaken. The sideEffects field in package.json can override the bundler’s analysis, but only if the library author has declared the side effects correctly .
Injection tokens in value positions. A component that is used as a token in @ContentChild or constructor injection cannot be tree-shaken. The lightweight injection token pattern replaces the class with an abstract class that is small and tree-shakeable .
Duplicated modules. A module that is imported by the initial chunk and by a lazy chunk is included in both. The bundler can extract it into a shared chunk that is loaded once. The analyzer shows the duplication .
Complete Example Session
This session demonstrates the tree shaking and bundle analysis workflow.
// ============================================
// PART 1: THE INITIAL IMPORTS
// ============================================
// Before optimization:
import * as _ from 'lodash';
import * as moment from 'moment';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatDialogModule } from '@angular/material/dialog';
import { MatTableModule } from '@angular/material/table';
// ... and every other Material module
// The namespace imports pull in the entire libraries.
// The Material modules pull in every component.
// The initial bundle is large.
// ============================================
// PART 2: THE OPTIMIZED IMPORTS
// ============================================
// After optimization:
import { debounce } from 'lodash-es';
import { format } from 'date-fns';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
// Named imports allow tree shaking.
// Only the used Material modules are imported.
// The initial bundle is smaller.
// ============================================
// PART 3: THE PRODUCTION BUILD
// ============================================
// ng build --configuration production --stats-json
// Output:
// Initial total | 1.45 MB
// Lazy | 420 kB
// - dashboard | 120 kB
// - admin | 180 kB
// The statistics file is written to dist/my-app/stats.json.
// ============================================
// PART 4: THE BUNDLE ANALYSIS
// ============================================
// npx webpack-bundle-analyzer dist/my-app/stats.json
// The treemap shows:
// - lodash: 70 KB (entire library)
// - moment: 230 KB (entire library)
// - @angular/material: 450 KB (all modules)
// - rxjs: 120 KB
// The large blocks are the optimization opportunities.
// ============================================
// PART 5: THE LODASH REPLACEMENT
// ============================================
// Before:
import * as _ from 'lodash';
_.debounce(fn, 300);
// After:
import { debounce } from 'lodash-es';
debounce(fn, 300);
// The lodash bundle drops from 70 KB to 2 KB.
// The lodash-es package is tree-shakeable.
// ============================================
// PART 6: THE MOMENT REPLACEMENT
// ============================================
// Before:
import * as moment from 'moment';
moment().format('YYYY-MM-DD');
// After:
import { format } from 'date-fns';
format(new Date(), 'yyyy-MM-dd');
// The moment bundle drops from 230 KB to 5 KB.
// The date-fns package is tree-shakeable.
// ============================================
// PART 7: THE MATERIAL MODULES
// ============================================
// Before:
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatDialogModule } from '@angular/material/dialog';
import { MatTableModule } from '@angular/material/table';
// ... every module
// After:
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
// Only the used modules are imported.
// The unused modules are not included.
// ============================================
// PART 8: THE SIDE-EFFECTS CONFIGURATION
// ============================================
// package.json
{
"name": "my-app",
"sideEffects": false
}
// The sideEffects: false tells the bundler that
// no file in the application has side effects.
// The unused files are removed.
// For libraries that have CSS imports:
{
"sideEffects": ["**/*.css"]
}
// The CSS files are preserved.
// The JavaScript files are tree-shakeable.
// ============================================
// PART 9: THE LIGHTWEIGHT INJECTION TOKEN
// ============================================
// Before (not tree-shakeable):
@Component({
selector: 'lib-card',
template: '<ng-content></ng-content>',
})
export class LibCardComponent {
@ContentChild(LibHeaderComponent) header?: LibHeaderComponent;
}
// After (tree-shakeable):
export abstract class LibHeaderToken {
abstract get header(): LibHeaderComponent;
}
@Component({
selector: 'lib-card',
template: '<ng-content></ng-content>',
providers: [{ provide: LibHeaderToken, useExisting: LibHeaderComponent }],
})
export class LibCardComponent {
@ContentChild(LibHeaderToken) header?: LibHeaderToken;
}
// The abstract class is small and tree-shakeable.
// The actual component is provided only when used.
// ============================================
// PART 10: THE RESULTS
// ============================================
// Before optimization:
// Initial total | 1.45 MB
// lodash | 70 KB
// moment | 230 KB
// material | 450 KB
// After optimization:
// Initial total | 380 kB
// lodash | 2 KB
// date-fns | 5 KB
// material | 180 KB
// The initial bundle is 73% smaller.
// The unused code is removed by tree shaking.
// The large libraries are replaced.
// The Material modules are limited.
The ten parts cover the initial imports, the optimized imports, the production build, the bundle analysis, the lodash replacement, the moment replacement, the Material modules, the side-effects configuration, the lightweight injection token, and the results.
Quick Reference
The Tree Shaking Concepts
| Concept | Definition |
|---|---|
| Tree shaking | Dead-code elimination |
| Side effect | Operation that affects outside its scope |
sideEffects | Package.json field declaring side effects |
/*@__PURE__*/ | Annotation marking a call as side-effect-free |
/*#__NO_SIDE_EFFECTS__*/ | Annotation marking a function as pure |
| Build optimizer | Angular plugin adding pure annotations |
The Bundle Analysis Tools
| Tool | Purpose |
|---|---|
--stats-json | Produce the statistics file |
webpack-bundle-analyzer | Render the treemap |
angular-esbuild-visualizer | Angular-specific analysis |
source-map-explorer | Alternative analyzer |
The Optimization Opportunities
| Opportunity | Fix |
|---|---|
| Namespace imports | Use named imports |
| Large libraries | Replace with tree-shakeable alternatives |
| Unused modules | Remove the import |
| Duplicated modules | Extract to shared chunk |
| Injection tokens | Use lightweight tokens |
The Side Effects Configuration
| Value | Effect |
|---|---|
false | All files side-effect-free |
true | All files have side effects |
["**/*.css"] | Only CSS files have side effects |
["polyfill.js"] | Only the specified file has side effects |
The Angular-Specific Techniques
| Technique | Purpose |
|---|---|
| Named imports | Enable tree shaking |
providedIn: 'root' | Tree-shakable providers |
| Lightweight injection tokens | Avoid token retention |
buildOptimizer | Add pure annotations |
| Lazy loading | Move code out of initial bundle |
Best Practices
✅ Do This:
// Use named imports
import { debounce } from 'lodash-es'; // ✅
// Declare side effects correctly
{ "sideEffects": ["**/*.css"] } // ✅
// Use lightweight injection tokens
export abstract class LibHeaderToken { } // ✅
# Build with statistics
ng build --configuration production --stats-json // ✅
# Analyze the bundle
npx webpack-bundle-analyzer dist/stats.json // ✅
❌ Don’t Do This:
// Don't use namespace imports
import * as _ from 'lodash'; // ❌
// Don't import large libraries without checking
import * as moment from 'moment'; // ❌
// Don't set sideEffects: true without reason
{ "sideEffects": true } // ❌
// Don't use a component as an injection token
@ContentChild(OtherComponent) other?: OtherComponent; // ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Bundle larger than expected | Namespace imports | Use named imports |
| Library not tree-shaken | No sideEffects field | Check the library’s package.json |
| Component not tree-shaken | Injection token in value position | Use lightweight token |
| CSS removed in production | sideEffects: false | Add CSS to the array |
| Duplicate modules | Shared dependency | Extract to shared chunk |
Real-World Examples
1. Named Import
import { debounce } from 'lodash-es';
2. Side Effects Configuration
{ "sideEffects": ["**/*.css"] }
3. Lightweight Injection Token
export abstract class LibHeaderToken { }
4. Build with Stats
ng build --stats-json
5. Bundle Analyzer
npx webpack-bundle-analyzer dist/stats.json
6. Angular Visualizer
npx angular-esbuild-visualizer dist/my-app/browser
7. Replace Moment
import { format } from 'date-fns';
8. Limit Material Imports
import { MatButtonModule } from '@angular/material/button';
9. Pure Annotation
const TOKEN = /*@__PURE__*/ new InjectionToken('desc');
10. Tree-Shakable Provider
@Injectable({ providedIn: 'root' })
Visual
The Tree Shaking Process
┌──────────────────────────────────────────────┐
│ TREE SHAKING │
│ │
│ Entry point (main.ts) │
│ │ │
│ ├──> Import A ──> Export A1 (used) │
│ │ Export A2 (unused) ✂ │
│ │ │
│ ├──> Import B ──> Export B1 (used) │
│ │ Export B2 (unused) ✂ │
│ │ │
│ └──> Import C (unused) ✂ │
│ │
│ The bundler removes the unused exports. │
│ The result is a smaller bundle. │
│ │
└──────────────────────────────────────────────┘
The Side Effects Decision
┌──────────────────────────────────────────────┐
│ SIDE EFFECTS DECISION │
│ │
│ Is the export used? │
│ └─ YES → Include │
│ └─ NO ↓ │
│ │
│ Does the module have side effects? │
│ └─ YES → Include │
│ └─ NO → Remove │
│ │
│ The sideEffects field declares the answer. │
│ │
└──────────────────────────────────────────────┘
The Bundle Analyzer Treemap
┌──────────────────────────────────────────────┐
│ BUNDLE ANALYZER │
│ │
│ ┌────────────────────────────────────────┐ │
│ │ @angular/material (450 KB) │ │
│ │ ┌──────────┐ ┌──────────┐ ┌────────┐ │ │
│ │ │ button │ │ card │ │ dialog │ │ │
│ │ │ 30 KB │ │ 25 KB │ │ 80 KB │ │ │
│ │ └──────────┘ └──────────┘ └────────┘ │ │
│ ├────────────────────────────────────────┤ │
│ │ moment (230 KB) │ │
│ ├────────────────────────────────────────┤ │
│ │ lodash (70 KB) │ │
│ ├────────────────────────────────────────┤ │
│ │ rxjs (120 KB) │ │
│ └────────────────────────────────────────┘ │
│ │
│ The large blocks are the opportunities. │
│ │
└──────────────────────────────────────────────┘
The Optimization Results
┌──────────────────────────────────────────────┐
│ BEFORE vs AFTER │
│ │
│ Before: │
│ Initial total | 1.45 MB │
│ lodash | 70 KB │
│ moment | 230 KB │
│ material | 450 KB │
│ │
│ After: │
│ Initial total | 380 KB │
│ lodash-es | 2 KB │
│ date-fns | 5 KB │
│ material | 180 KB │
│ │
│ The initial bundle is 73% smaller. │
│ │
└──────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Tree shaking | Dead-code elimination |
| Side effect | Operation affecting outside its scope |
sideEffects field | Declares which files have side effects |
/*@__PURE__*/ | Marks a call as side-effect-free |
buildOptimizer | Adds pure annotations to Angular decorators |
| Lightweight injection tokens | Avoid token retention |
| Bundle analyzer | webpack-bundle-analyzer, angular-esbuild-visualizer |
| Statistics file | --stats-json |
| Optimization | Named imports, library replacement, lazy loading |
Key takeaways:
- Tree shaking is dead-code elimination. The bundler starts from the entry point, crawls the imports, and removes any export that is not reachable. The static structure of ES modules makes this possible. Namespace imports prevent tree shaking. Named imports enable it .
- Side effects prevent tree shaking. A module with top-level code that runs on import cannot be removed. The
sideEffectsfield inpackage.jsondeclares which files have side effects. A library that marks itself assideEffects: falseallows the bundler to remove every unused export . - The
buildOptimizerplugin adds/*@__PURE__*/annotations to Angular decorators. The minifier cannot tell whether a decorated class has side effects. The annotation tells it that the class is safe to remove if it is not referenced . - Injection tokens in value positions prevent tree shaking. A component used in a
@ContentChilddecorator or constructor injection is retained at runtime. The lightweight injection token pattern replaces the class with a small abstract class that is tree-shakeable . - The bundle analyzer shows what is in the bundle. The
--stats-jsonflag produces the statistics file. Thewebpack-bundle-analyzertool renders the treemap. Theangular-esbuild-visualizertool distinguishes the initial download from the lazy routes . - The large blocks in the treemap are the optimization opportunities. A large library that is not tree-shakeable can be replaced with a lighter alternative. A module that is not needed on the initial route can be lazy-loaded. A module that is duplicated can be extracted to a shared chunk.
- The combination of tree shaking and bundle analysis produces a lean bundle. Tree shaking removes the unused code. The analysis shows what remains. The developer decides what to optimize. The production build is the artifact that the user downloads.
Remember: Tree shaking removes the code the application does not use. Bundle analysis shows what is left. The two tools work together. The sideEffects field declares the side effects. The /*@__PURE__*/ annotation marks the pure calls. The lightweight injection tokens avoid retention. The named imports enable removal. The statistics file feeds the analyzer. The treemap shows the opportunities. The production build is the result.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!