Reducing Angular Library Contributions to the Main Bundle

  |   Angular

In this guest post by Nicolas Gehlert, origianlly published here, learn the best way to include AG Grid in your own shared Angular library that will not result in AG Grid being included in the main bundle, as long as it is only used in lazy loaded routes.

Many thanks again to our user Nicolas for sharing his findings with us!


Using an Angular workspace with your own library you might've noticed that your main bundle size keeps increasing the more features you are adding - even if you
are not using all features up front. Let me show you how you can build a sustainable and lightweight library structure for your own library.

Introduction

Angular libraries are awesome and Angular does a good job giving you a kick start with their documentation.
However this only works for very simple and small libraries. If your library gets bigger and you want to optimize it, I found
there to be very few good resources.

Let me tell you an example. I'm using a big library with dependencies like AG Grid to provide shared components and services
in a multi application Angular workspace. I'm using lazy loading for all my routes but I discovered the main bundle get's increased
for all applications if I add more things to the library. At some point I was just using a simple data service in the main
application and an AG Grid component few lazy loaded modules down the line, but still AG Grid and all components were added to the
main bundle.
In this post I will show you how to analyze your package and how to properly set up your library in order to prevent similar issues.

All examples in this post use a sample project at https://github.com/ngehlert/angular-lazy-load.

Bundle analyzer

A bundle analyzer allows you to get insights in your bundled JavaScript. It shows you what dependencies are included and how much space
each section needs.
The Angular team recommends to use a tool called source-map-explorer. A lot of other post show tools like webpack-bundle-analyzer
or others. To some extend they work as well, but let's stick with the source-map-explorer for now.

First we need to install it with npm or yarn

npm install -g source-map-explorer
yarn global add source-map-explorer

Now build your project with the --source-map=true flag. (If you are using a project with an Angular version prior 12 also add the --prod flag)

ng build app --source-map=true

And now we can have a look at our stats with

source-map-explorer dist/app/main.js

The result looks something like this.

Original Bloated Main Bundle

Problem analysis

Let's have a look at our sample application. We are using a small service and a component to display a simple table

Original Library Structure

In our app.component.ts we are using the RandomService and in our lazy loaded route the TableComponent.
But if we have a look at our bundle analysis (see screenshot above) we will notice that AG Grid is actually added to the main module, instead of the lazy loaded one.

It turns out by default an Angular library is actually not tree shakeable, and you will always add the entire library to your bundle, even if you are just
importing one service or variable.

Solution - Multiple Entry points

You can create multiple entry points to your library and then you only need to import a specific entry point you are interested in, instead of the entire library.
Angular material is using the same concept, for example import {MatDialog} from '@angular/material/dialog';
Creating separate entry points is actually fairly simple and you only need to create a new directory inside your library and add an ng-package.json file.
Inside the ng-package.json you need to put the following content

{
    "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json",
    "lib": {
        "entryFile": "public_api.ts"
    }
}

In our example I moved the table directory into a separate grid directory next to the already existing src directory.

Library with multiple entry points

If you now run ng build good-lib you will notice that actually two entry points are created

Building Angular Package

------------------------------------------------------------------------------
Building entry point 'good-lib'
------------------------------------------------------------------------------
✔ Compiling with Angular sources in Ivy partial compilation mode.
✔ Writing FESM bundles
✔ Copying assets
✔ Writing package manifest
✔ Built good-lib

------------------------------------------------------------------------------
Building entry point 'good-lib/grid'
------------------------------------------------------------------------------
✔ Compiling with Angular sources in Ivy partial compilation mode.
✔ Writing FESM bundles

Now just one step is missing in order to use the secondary entry points. Go to your tsconfig.json (in your root directory). And extend the path entry for your library with a
placeholder match.
before:

"paths": {
  "good-lib": [
    "dist/good-lib"
  ],
},

after:

"paths": {
  "good-lib/*": [
    "dist/good-lib/*"
  ],
  "good-lib": [
    "dist/good-lib"
  ],
},

Note: You just need to do this once for your first secondary entry point. All future entry points will be resolved as well.

You can now use the TableModule from the secondary entry point like this import { TableModule } from 'good-lib/grid';

How to reference between the different entry points

You can NOT use relative imports between difference entry points. You need to import from the build package like you would from a regular application
as well. In our example this would be import {RandomService} from 'good-lib'; if you want to use it in the grid entry point.

Note: ng-packagr will detect in which order it needs to compile the different entry points so everything can be resolved. You will get an
error if you introduce a circular dependency and two (or more) entry points actually try to import from each other.

Test for yourself

You can clone the https://github.com/ngehlert/angular-lazy-load and just need to change three imports
to switch between the good and the bad library. See what imports to change

In the end with the good library AG Grid gets added to the lazy loaded route instead of the initial bundle. Now our initial bundle is only 200kb instead of 1.3mb!

For further details about minimizing the AG Grid bundle size in your application make sure to also read our other post Minimising Application Bundle Size

Read more posts about...