Improving bundle size on Angular apps

August 11, 2020

Even though Angular has multiple core technologies which improve with no-effort how our bundle is generated, it's not strange size becomes a problem that require some sort of special caring. In this post we are going to analyze basic tips that ultimately lead to a lighter and snappier Angular application.

Lazy Loading

We usually build our Angular apps as a collection of modules. These modules represent a cohesive block of code dedicated to an application domain or even a closely related set of capabilities.

Lazy loading benefits of this modularity by splitting the application bundle in smaller chunks that represent the code of these modules. Angular's router then takes care to load them as soon as a user navigates to a route served by the lazy-loaded module.

The following image represents the resulting artifacts built after choosing to lazy-load some of the modules of an existing app:

Lazy Loading diagram

Main bundle file contains Angular and the core modules that help bootstrap our app. Feature-modules are separated in different chunks that are only loaded if the user navigates to a route resolving to an inner component of it.

As you might imagine, it's fairly simple to implement lazy loading on a existing app as long as you already have a proper module architecture. The only requirement is to split main app router into separate routers that are part of each lazy-loaded module.

Sample use case

Let's put in practise this concept with a sample use case: Our Angular app has one module, UserModule which includes components linked from the AppComponent. Since none of these routes are accesible until the app has been loaded, it's a clear candidate to be lazy-loaded.

We start by modifying the App module router to define which route segments will be served by lazy loaded modules. In the following use-case any route below user will be resolved by the child router, part of UserModule.

const appRoutes: AppRoutes = [	
	{ path: '', component: HomeComponent },
	{ path: 'user',
      loadChildren: () => import('./user/user.module').then(m => m.UserModule) }
];

Child module router contains only routes which are resolved by components part of that module. In our use-case, UserProfileComponent and UserPreferencesComponent.

const routes: UserRoutes = [
	{ path: '', component: UserProfileComponent },
	{ path: 'preferences', component: UserPreferencesComponent }
];

Last but not least we remove the static import we had on the AppModule definition for the UserModule as the Angular router is now in charge of dynamically loading it when is needed.

As soon as the user navigates to any of the routes resolved by the child router Angular will dynamically load the chunk that represents code from this module.

Lazy Loading demo

Angular Ivy and Differential Loading

Back in 2019, Angular team introduced two key features in subsequent releases that really made a change in terms of bundle size and performance:

At the end enabling each of these improvements is a matter of configuration if you're migrating from an old release, while it might be already enabled for newer applications generated using Angular CLI.

While Differential Loading should be an innocuous change, Angular Ivy migration might result a bit tricky and require some work specially by dealing with third party libraries not updated to support modern versions of Angular. It's up to you to decide which path to go!

Angular Ivy size improvements

Webpack Bundle Analyzer

If you already implemented Lazy Loading, and you're using either Angular Ivy or Differential Loading but you feel your bundle size is still too high, you might be interested on checking how the bundle is being built to seek for additional improvements. Webpack Bundle Analyzer provides a nice and interactive visualization of the bundle Angular generates, making a joy to find out what module is candidate to lazy-load or which third party library we should avoid using due to its large footprint.

Using it is kind of easy, just install it with npm i webpack-bundle-analyzer --save-dev and then generate a build of your Application with the --stats-json parameter.

The only thing left to do is to run the Webpack Bundle Analyzer specifying the json file generated as a result of the previous parameter: npx webpack-bundle-analyzer dist/stats.json. A URL will be provided so you can dive into the bundle generated for your app.

Generated bundle analysis

Angular CLI budgets

Let's imagine for a moment you just finished optimising bundle size of your Application, and you want to follow up any kind of future change that has direct impact on it. Angular CLI provides us a helpful feature to track any change on bundle size by establishing limits or budgets where the build process will raise warnings or even errors if generated artifact size goes way beyond your predefined values.

Configuring them is really straighforward, you just need to add a budgets array per each of the environments you have configured in your angular.json file. Within a budget you can specify a type of it (as you might want to track only some of the artifacts generated), a name and all the size limits you want to set for it.

"budgets": [{
    "type": "bundle",
    "name": "main",
    "maximumWarning": "170kb",
    "maximumError": "250kb"
}]

Whenever a budget is hit you will get visual notification of it during the build process, forcing you to go through the latest changes you made to analise which one is causing the bundle size to grow.

Closing thoughts

Obviously I won't recommend anyone to go reckless and upgrade directly to Angular Ivy at this point, but Differential and Lazy Loading are something that you should look into when thinking on reducing your application bundle size. Specially Differential Loading as it is just a matter of configuration for a really valuable saving in bandwidth both for the user and the server.

If you still face issues with bundle size, just fire up Webpack Bundle Analyzer, and find out what is hogging your Bundle: Most of the times are third party component libraries which are not correctly imported. Remember to always set size budgets to prevent increasing your app footprint in an uncontrolled way.

Remember to take a look to the official Angular Documentation to find out further information on how these concepts can be applied to your existing applications.

About the author: Oscar Rodríguez

Friendly Software Engineer at mimacom. Cries when he sees code wrapped by tons of empty lines.

Comments
Join us