Tree Shaking & Lazy Loading Pitfalls

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.

Investigation Tips & Tricks

There are a few things to keep in mind while investigating lazy loading & tree shaking topics.

Optimization flags

Ensure that the optimizations (buildOptimizer & optimization) are turned on for Angular. This also works directly with serve.

Source Maps setup

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.

Analysing the bundle

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.

Tree Shaking Guidelines

In general, tree shaking works fine, but there are significant caveats that need to be kept in mind.

Splitting Code

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.

Mark Code as Side Effects Free

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
}

Examples

Tree Shaking in the Same File/ES Module

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();
}

Breaking the Tree Shaking

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.

Fixing the broken Tree Shaking

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.

results matching ""

    No results matching ""