Angular is an awesome framework. We all love it 😍
One thing that makes Angular successful and excellent at the same time is the vast community and the value it provides. There are a lot of Angular meetups, blogs, conferences, and of course, libraries.
Nowadays, thanks to the Angular CLI, libraries are easy to create. They are a great way to share code across multiple applications.
Since they can be used in many places, performance is a critical aspect. A library that doesn’t perform can slow down multiple applications!
In Frontend, there are different types of performance. Runtime performance and initial load. In this article, we will focus on initial load.
While providing and maintaining various libraries and a UI framework for a big enterprise, I encountered some not so apparent pitfalls and ways to deal with them. I think it’s worth sharing some of those experiences.
💬 “It’s such a simple library. It can’t affect performance, right?”
Let’s start with a simple library that we generate with the help of the Angular CLI. In case you have never created an Angular library, it might be helpful to read the following article:
The ultimate guide to set up your Angular library project
Automated code formatting with Prettier & Husky, test coverage reports, deployed showcase and fully automated releases…
Once we used the CLI to set up the multi-project workspace, we can start to add some code.
The library is called
howdy and it’s only purpose is to greet you with your name or to tell you the local time. It contains two modules with each a component. One module to greet and one to tell the time.
Just a simple Angular module and a Component that accepts a name property over Input bindings and displays it.
HowdyTimeComponent is responsible for displaying the time with the help of the third-party library
Great! Our howdy library is ready to be published! It’s such a simple library; it can’t affect performance, right?
Consume the howdy library 🤠
The howdy library is out! It would be a shame not to take advantage of it. 😉 To use the howdy library, we generate a brand new SPA with the Angular CLI.
ng new greeting-app
Since we are interested in performance, let’s also install a dev dependency called
npm i -D webpack-bundle-analyzer
webpack-bundle-analyzerallows you to visualize size of webpack output files with an interactive zoomable treemap.
The best way to analyze our bundle is to add the following analyze script to our
"analyze": "ng build --prod --stats-json && webpack-bundle-analyzer ./dist/greeting-app/stats-es2015.json"
If we run this command, Angular performs a production build and also outputs a
stats-es2015.json which is then picked up and visualized by
Since we haven’t written any code yet, our main bundle mainly consists of Angular. We can also see that
zone.js is included in our
Altogether, the size of our application is now at
But we haven’t yet included our howdy library! Let’s go ahead and do so.
npm i howdy
We installed the
howdy library because we want to display a greeting with a name. We are not interested in presenting the time. Therefore we are only going to use the
HowdyNameModule and we do not include the
The vital thing to notice here is that we only import the
HowdyNameModule. Let’s rerun the
Wow! Pretty boiled! We went from
207 KB to
511.15 KB. More than double the size. What the f…! 😳
One look is enough to find the culprit.
moment is huge! It brings its core implementation and all the locales.
moment can be replaced with other packages like
moment-mini. But the real question is; Why is it even there? Remember that we only imported the
HowdyNameModule and not the
HodwyTimeModule. I thought tree shaking shakes away unused modules? 🤔 What’s going on?
Not everything can be tree shaked 🎄
To make tree shaking possible, the Angular build runs a bunch of advanced optimization. But still
moment does occur in the bundle even if the
The problem lies in the way
moment is packaged. Let’s have a quick look at
moment.js file in our
UMD and not as
UMD bundled libraries are wrapped in an IFFE, which means that
ModuleConcatenation can't be used. There’s no way for the build optimization tools to know if this code will be used or has side effects.
In a nutshell, this type of module prevents Angular from running the more advanced bundle optimizations.
Unfortunately, we can’t control how
moment is packaged. Does this mean we need to accept this huge bundle size?
Secondary entry points for the win 🤩
We can’t control how
moment is built. But we can manage our library. And indeed, there’s a way to prevent such scenarios. Secondary entry points!
Nowadays, almost all Angular libraries are packaged with
ng-packagr allows you to use an
ng-package.json in combination with a
public-api which will end up as an entry point to your application.
As the name indicates, secondary entry points allow you to specify multiple entry points to your application.
Sounds good! 👍 How to enable secondary entry points?
Secondary entry points are dynamically discovered by
ng-packagr searches for
package.json files within subdirectories of the main
package.json file's folder.
Cool! Let’s take advantage of secondary entry points in our
howdy library by adding the following files.
For each module, we added a
package.json and a
index.tsis just there to point to the
public_apiwhich is helpful during imports.
public_apiexports all the modules and components from our module.
ng-packagrspecific configurations. In our example, it’s enough to specify the
package.jsoncan also contain other properties like
cssUrletc… Notice that the scope of those properties is just the current subentry.
If we now run a build, we would end up with three chunks.
howdy-src-lib-name.js now only contains the
HowdyNameModule related code, and the
howdy-src-lib-time.js now only contains the
HowdyTimeModule specific code.
But let’s have a look at the
howdy.js chunk still contains
HowdyTimeComponent. Means, we still get
moment even if we only import the
If we want to shake away
HowdyTimeModulewith the current approach, we would need to use deep imports. So that we don’t import from
howdy.jsbut directly from
Which is not recommended! Deep imports are dangerous and should always be avoided!
How can we address those issues? How can we ensure that the
HowdyTimeModule also get shaken away even if we use standard imports? Well, we need to adjust the way the
howdy.js chunk is created.
Use a “signpost”
The idea is removing the code from the
howdy.js chunk and instead let it act as a sort of “signpost” which points you to the other chunks.
Let’s, therefore, have a closer look at
* Public API Surface of howdy
*/export * from './lib/name/howdy-name.component';
export * from './lib/name/howdy-name.module';
export * from './lib/time/howdy-time.component';
export * from './lib/time/howdy-time.module';
Those lines are responsible for including everything from
time in the
howdy.js chunk. We need to remove the code from
howdy.js and let it point to the other fragments which contain the implementation. Let’s adjust its content.
* Public API Surface of howdy
*/export * from 'howdy/src/lib/name';
export * from 'howdy/src/lib/time';
Instead of exporting the real implementation, we put a relative path to the different chunks. With this change the
howdy.js only points to other bundles and does not include any “real” code.
ng build and analyze our
howdy.js now acts as “signpost” which points to the chunks that contain the implementation. The
howdy-src-lib-name.js chunk only contains the code from the
name folder and the
howdy-src-lib-time.js only contains the code from the
Consum a package with Subentries
Let’s update the
howdy package in our
greeting-app and rerun the
Cool. The size of the bundle is now at
170.94 KB. A little bit higher than initially. Let’s check out how the
howdy module looks like in the final bundle.
Great! This adjustment allows us to keep the bundle size of the consuming SPA small. The SPA only gets what they need!
Secondary entry points are very cool when you use them in combination with lazy loading. If we would use the
HowdyTimeModulein a lazy loaded module,
momentwould end up in a lazy loaded chunk and not in the main chunk.
Real-world experiences 👷
The example above is very straight forward.
However, it is different once you introduce secondary entry points in an existing enterprise project. You have to deal with a lot more complexity while the error messages from
ng-packagr are not always helpful.
Chances are high that you need to adjust some import paths or specify some path mappings in your
tsconfig.json. And you will also be confronted with modules from one chunk which use modules from another chunk.
But trust me, once you master those burdens, it will be worth it.
In a vast enterprise environment, we figured out that the bundle size of freshly created SPAs exploded after we included some of the libraries which we provided.
At some point, it even went up to
5MB. 😮 Every SPA got
@swimlane/datatable and other things it did not even use. We started to focus on optimizing this bundle size.
date-fns and started to use secondary entry points. We currently ended up with a main chunk of
662 KB for a freshly created SPA that includes some libraries. It’s still big, but, we are not yet there. The optimization is not yet finished — we can reduce the bundle size even more.
It’s quite cool to see where we are now and where we came from. 💪
However, while it’s quite easy to take advantage of secondary entry points in the example above, it can be quite cumbersome to introduce them to a more significant project.
Angular does a remarkable job when it comes to optimizing bundle size. Even though those build optimization steps are very sophisticated, they can not tree shake everything.
Modules packaged in other formats than
ESModules can not be tree shaked.
As library creators, we should, therefore, carefully watch the effects of our library on bundle size when we include third-party libraries.
We can’t control how third-party libs are packaged. But we are very well in control of how our library is packaged.
Subentries offer us an excellent way to deliver our library in multiple chunks. Those chunks are tree shakeable during Angulars build optimization. With this approach, even wrongly packaged third party libraries only get included in the final bundle if they are used.
🧞 🙏 If you liked this post, share it and give some claps👏🏻 by clicking multiple times on the clap button on the left side.
Claps help other people to discover content and motivate me to write more 😉
Feel free to check out some of my other articles about front-end development or download one of my open-source modules!