Unfortunately, tree shaking and lazy loading contain a few pitfalls that make it easy to end up with a larger main bundle size than intended.
And while they are two different things, they are also tightly connected, which is why we look into them together here.
There are a few things to keep in mind while investigating lazy loading & tree shaking topics.
Ensure that the optimizations (buildOptimizer
& optimization
) are turned on for Angular. This also works directly with serve
.
Turn off source maps in the browser dev tools. For example in Chrome. The source maps do contain code that is actually not in the bundle, so this would lead to misleading results. An alternative could be to turn off the source maps in the tsconfig.json
.
Use the global search in the browser dev tools to search for code being present. Keep in mind that minification changes the code, so search for unchangeable things, like certain strings.
While navigating through an app, use the search again to check if certain code was properly lazy loaded or if it was already present in the main bundle.
In general, tree shaking works fine, but there are significant caveats that need to be kept in mind.
Splitting code into separate files/ES Modules helps the tree shaking to not bundle up unrelated code. Code that is not supposed to always be bundled up together should therefore be separated.
See also, the more detailed example below on the effects of this.
The webpack documentation contains a section about tree shaking and side effects: https://webpack.js.org/guides/tree-shaking/
The short summary for our context is, to always ensure that shared libraries are marked as side effects free in their package.json
file
ng-packagr does that automatically for built and published Angular libraries. But in monorepo/nx
workspaces, this has to be done manually for local usage.
For that, ensure that there is a package.json file with the sideEffects
flag set to false
, either in the library root or up in the directory hierarchy of the repository.
{
"sideEffects": false
}
Let's take a closer look at the tree shaking behavior with a concrete example, based on this fix in the taly-tutorials app.
In this example, we have a class and a function located in the same file/ES Module. The function is imported by the main application. The class is not used anywhere.
The tree shaking will correctly remove the class from the main bundle and only keep the function.
// The PFE import is not included in the main bundle
import { PfeBusinessService } from '@allianz/ngx-pfe';
import { Injectable } from '@angular/core';
const COMPLETE_TUTORIAL_ACTION_TYPE = 'COMPLETE_TUTORIAL_MODULE';
@Injectable()
export class MyCustomClass {
constructor(private pfeBusinessService: PfeBusinessService) {
// We put a string here to easily be able to find the class in the optimized and minimized code
console.log('MyCustomClass');
}
}
// This will not be removed by the tree shaking as it is located in the global context of the file/module
console.log('this is not shaken');
export function myCustomFunction(aParameter: string): boolean {
'myCustomFunction'.console.log();
}
Unfortunately, it is quite easy to break the tree shaking.
Let's add a new file/module to our app. This module will be lazy loaded in our app. It has the following content:
import { NgModule } from '@angular/core';
import { MyCustomClass } from './my-custom-module';
@NgModule({
providers: [MyCustomClass],
})
export class MyCustomModule {
constructor() {
console.log('MyCustomModule');
}
}
Now, suddenly MyCustomClass
ends up in the main bundle. MyCustomModule
however is not part of the main bundle.
MyCustomClass
's code would also end up in the main bundle with this example:
import { NgModule } from '@angular/core';
import { MyCustomClass } from './my-custom-module';
@NgModule({
providers: [],
})
export class MyCustomModule {
constructor() {
console.log('MyCustomModule');
const myInstance = new MyCustomClass();
}
}
In both cases, the expectation was that MyCustomClass
would be part of the lazy loaded bundle. But the fact that MyCustomClass
is defined in the same file as other code (myCustomFunction
) that is meant to be in the main bundle, leads to webpack adding MyCustomClass
to the main bundle.
To prevent MyCustomClass
(and with it the PFE) from being included in the main bundle, we simply have to split MyCustomClass
and myCustomFunction
into separate files/modules. Then myCustomFunction
can still be used in the main bundle and MyCustomClass
will only end up in the lazy loaded bundle.