Since the release of the latest Angular version, I have been curious about how tree shaking behaves, particularly after the introduction of standalone components. The outcome took me by surprise, so let's explore what has changed and what steps you can take to speed up your builds and trim your production bundle.
The Concept of Tree Shaking
Before diving into the details, it’s a good idea to define tree shaking. Modern build tools are capable of eliminating unused or dead code from the final production bundle. This process is what we call tree shaking. Most JavaScript toolchains now include tree shaking support as a standard feature.
Consider the following example, which defines two functions, add and sub.
export function add(a,b){
return a+b;
}
export function sub(a,b){
return a-b;
}
If our app only relies on the add function, what becomes of sub? The build tool recognizes that it is unnecessary and strips it out of the production bundle. The result is that only the code we actually use, in this case add, makes it into the final output.
Understanding Nx
This article isn't centered on Nx, but since we’ll be working with it, it’s worth clarifying what it is. Nx is a build tool designed with monorepo support, offering capabilities to reduce build times and optimize CI efficiency.
Angular Secondary Entry Points
Another term that will come up throughout this discussion is secondary entry points. If you’re unfamiliar with the idea, I recommend checking out the stream on YouTube that I hosted previously.
Tree Shaking with Angular Libraries
For quite a while, tree shaking in Angular libraries has been made possible through secondary entry points, a feature facilitated by ng-packagr.
When working with Nx, there is a dedicated generator for secondary entry points that you can use with your libraries.
Most developers rely on these tools to eliminate dead code from their Angular applications or to enable tree shaking.
But what if that's no longer a requirement? It’s not a sweeping claim, but rather the reality that secondary entry points are only sometimes necessary for tree shaking.
It is recommended to use standalone components by default. I am demonstrating with modules in case you are not yet able to use standalone components. This means you are on a version prior to Angular 14. You won't see the benefits of tree shaking right away, but you can ensure your code is prepared for the future. Once you move to Angular 16, tree shaking will work automatically.
Examining the Code
To get a full picture of how tree shaking works, it helps to write some code. For this demonstration, I'll use an Nx workspace since it's my preferred method for starting new projects, allowing me to manage multiple projects with ease.
Begin by creating a new app.
npx create-nx-workspace
✔ Where would you like to create your workspace? · tree-shaking-demo
✔ Which stack do you want to use? · angular
✔ Integrated monorepo, or standalone project? · integrated
✔ Application name · hello-world
✔ Which bundler would you like to use? · esbuild
✔ Default stylesheet format · css
✔ Do you want to enable Server-Side Rendering (SSR) and Static Site Generation (SSG/Prerendering)? · No
✔ Test runner to use for end to end (E2E) tests · none
✔ Which CI provider would you like to use? · skip
✔ Would you like remote caching to make your build faster? · skip
Library with a Single Module for Multiple Components
In large codebases, I often see what I call a "God module." This is a module that re-exports all components from a library. Instead of just talking about it, let's look at the code to understand this concept better.
With Nx, libraries are typically kept in the libs folder. Interestingly, starting with Nx 16, you can actually place your libraries anywhere in the workspace. To create our project, run the command below to generate god-module-lib. Notice we are using the standalone=false flag because we want to use a module for this library.
npx nx generate @nx/angular:library --directory=libs/god-module-lib --buildable=true --name=god-module-lib --standalone=false --no-interactive
Let's add a few placeholder components to this library and export them through the module.
npx nx generate @nx/angular:component --path=libs/god-module-lib/src/lib/table/table --export=true --standalone=false --module=god-module-lib.module.ts --no-interactive --dry-run
npx nx generate @nx/angular:component --path=libs/god-module-lib/src/lib/button/button --export=true --standalone=false --module=god-module-lib.module.ts --no-interactive --dry-run
Now, let's use this module in our application. Since the app is created with the standalone flag, there are no modules by default. We'll import GodModuleLibModule into app.component.ts.
import { GodModuleLibModule } from '@tree-shaking-demo/god-module-lib';
@Component({
imports: [ RouterModule, GodModuleLibModule],
})
export class AppComponent {
title = 'hello-world';
}
Then, we'll use the button component in the template, which is located in app.component.html.
<lib-button/>
After that, we can run the build with the sourceMap flag, which we'll need to create the bundle report:
nx run hello-world:build:production --sourceMap
npx source-map-explorer dist/apps/hello-world/browser/*.* --html report/hello-world.html
When you open the report, you'll see 395 Bytes of code originating from GodModuleLibModule. This includes only the button component code that is actually used in our template. Try this same test with your Angular version. I ran it on Angular 14 and found code for both the Button and Table components.
Angular is getting better at identifying code that is not being used. Even if your code has a GodModule that exports multiple components, your application will still see the benefits of tree shaking.
However, there is a new issue: What if the Button and Table components have their own modules which are being re-exported from GodModuleLibModule?
Refactoring the Library to Use Multiple Angular Modules
In this next experiment, we'll split the Table and Button components so they each have their own module, and then re-export both from the GodModuleLibModule.
We'll add a ButtonModule in the button folder and a TableModule in the table folder.
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ButtonComponent } from './button.component';
@NgModule({
imports: [CommonModule],
declarations: [ButtonComponent],
exports: [ButtonComponent],
})
export class ButtonModule {}
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { TableComponent } from './table.component';
@NgModule({
imports: [CommonModule],
declarations: [TableComponent],
exports: [TableComponent],
})
export class TableModule {}
Then, we'll update the index.ts file.
export * from './lib/god-module-lib.module';
export * from './lib/button/button.component';
export * from './lib/button/button.module';
export * from './lib/table/table.component';
export * from './lib/table/table.module';
Let's run the build again and check the report.
nx run hello-world:build:production --sourceMap
npx source-map-explorer dist/apps/hello-world/browser/*.* --html report/hello-world-individual-module.html
Surprisingly, the bundle size has nearly doubled to 701 Bytes. You can now see the code for the table. This module is included in your bundle even though it's not being used.
Let's move on to another test: we'll remove the module entirely and switch to a standalone component.
Migrating the Library to Use Standalone Components
Angular offers a migration tool for converting to standalone components, but since this is a small app, we can do it by hand. We'll delete the GodModuleLibModule, ButtonModule, and TableModule files.
Next, open button.component.ts and table.component.ts and modify the lines as shown below.
standalone: false --> true,
After that, update your index.ts.
export * from './lib/button/button.component';
export * from './lib/table/table.component';
This looks much cleaner; we're simply importing the components now.
Next, go to your app and update the import in app.component.ts.
import { GodModuleLibModule } from '@tree-shaking-demo/god-module-lib';
To
import { ButtonComponent } from '@tree-shaking-demo/god-module-lib';
and
imports: [GodModuleLibModule --> ButtonComponent],
We're ready to run the build and report again. This time, we have only 234 Bytes, which contains just the Button code.
nx run hello-world:build:production --sourceMap
npx source-map-explorer dist/apps/hello-world/browser/*.* --html report/standalone.html
Working with a Publishable Library
Let's create a library and publish it to see how tree shaking behaves in that scenario.
npx nx generate @nx/angular:library --directory=libs/ng-publishable --publishable=true --importPath=@ngx-santosh/ng-publishable --no-interactive
We'll add a few components, sticking with the standalone component approach:
npx nx generate @nx/angular:component --path=libs/ng-publishable/src/lib/accordion --export=true --no-interactive
npx nx generate @nx/angular:component --path=libs/ng-publishable/src/lib/grid --export=true --no-interactive
Now, let's publish the library and use the GridComponent in our application.
import {GridComponent, AccordionComponent} from '@ngx-santosh/ng-publishable';
@Component({
imports: [GridComponent],
selector: 'app-root',
templateUrl: './app.component.html',
styleUrl: './app.component.css',
})
export class AppComponent {
title = 'hello-world';
}
And add the Grid component to our template.
<lib-grid/>
Let's run the build and generate the bundle report again.
nx run hello-world:build:production --sourceMap
npx source-map-explorer dist/apps/hello-world/browser/*.* --html report/hello-world-publish-libs.html
We still get tree shaking with publishable libraries. The final bundle contains only 248 Bytes of the grid component code, and as you add more components, you only pay for what you include.
Another benefit of using secondary entry points is that if you rely on a third-party library like lodash, Angular can remove it if the component using it is not in your final code. But what if lodash is used but you don't have a secondary entry point? Will tree shaking still function? The good news is yes, it works even without the secondary entry point
Wrapping up
- For projects still on Angular 14 or earlier, the techniques demonstrated here might not shrink your bundle the same way. Still, moving toward standalone components is a good first step. Once you're on Angular 15, running
ng g @angular/core:standalonehelps you keep only the necessary code in your production output. - The recommendation above applies equally to both your own application code and any libraries you maintain.
- When dealing with libraries that expose a large number of components,
- Splitting them into several smaller libraries can be helpful.
- If the library is meant for publishing, leaning on secondary entry points is the way to go.
- The recent Angular language service flags an IDE warning any time you import a component you don't end up using.
- The updated Angular compiler can also detect unused imports during the build process and let you know.
It's encouraging to see the Angular compiler steadily getting better—there's less need to rely on secondary entry points for libraries to benefit from tree shaking. So why wait? Upgrade to a recent Angular release to enjoy smaller bundles and more effective tree shaking.
It's also nice that developers can put more energy into writing features rather than worrying about whether their libraries will shake out correctly under Angular.
The source code from this walkthrough is available here:
Go ahead, run this against your own codebase and let us know how it went—were you and your team able to cut out some kilobytes from your production bundle?
A big thank you to my GitHub Sponsors for supporting my Open Source work.




