How To Improve the Performance of Angular Apps?

Konstantin Dinev / Friday, August 4, 2023

Angular has become a very popular and widely adopted framework for developing modern web applications. This technology is both very powerful and feature-rich. Everything that you need as a web developer comes out-of-the-box and Angular allows for easily configuring, maintaining, and expanding any application built on top of the framework.  

And by now, you’ve probably already put together one or more Angular applications, but are they optimal?  

In Part 2 of my Software Performance series, then, I will talk about Angular optimization, demonstrating how to improve the performance of an Angular application using an Angular example app that I built. I will use Chrome Dev Tools to derive an initial lighthouse score to determine where the application initially stands. Let’s take a look at what can be improved and how. 

Other blogs from this series:

Software Performance [Web] Part I

Quick Article Overview:

How to Improve The Performance of Angular Applications 

For this article, I will use a sample Angular application that I’ve put together. At the time of writing this article, the application uses the following features and libraries: 

  • Angular 16
  • Ignite UI for Angular 16
  • RxJS 7
  • PWA (Angular service worker)
  • Server-side rendering (express server)
  • Angular localization

Angular Build

Everything seems to be running just fine when I’m running the application in a development environment, but the initial lighthouse score is not very high:

 Lighthouse score with development environment run

When I look at the suggestions for improving the lower-scoring sections, I see where the issues are coming from. The first big issue is the size of the resources (JavaScript, styles, static resources) that are transferred to the client. 

 Opportunities for improving performance of the Angular app

This issue is resolved easily by running a production build of my Angular application rather than a development one. Always build with production configuration prior to deployment. This will resolve the warning to reduce the size of JavaScript and CSS. Let’s take a look at theangular.jsonfile at the root of our Angular repository to see how the production build differs: 

"configurations": {
  "production": {
    "budgets": [
      {
        "type": "initial",
        "maximumWarning": "2mb",
        "maximumError": "5mb"
      },
      {
        "type": "anyComponentStyle",
        "maximumWarning": "10kb"
      }
    ],
    "fileReplacements": [
      {
        "replace": "projects/common/src/environments/environment.ts",
        "with": "projects/common/src/environments/environment.prod.ts"
      }
    ],
    "localize": true,
    "optimization": true,
    "outputHashing": "all",
    "sourceMap": false,
    "namedChunks": false,
    "extractLicenses": true,
    "vendorChunk": false,
    "buildOptimizer": true,
    "serviceWorker": true,
    "i18nMissingTranslation": "error",
    "ngswConfigPath": "projects/bellumgens/src/ngsw-config.json"
  },
  "bg": {
    "localize": [
      "bg"
    ]
  }
}

There’s quite a lot of configuration here. However, the most important one, in this case, is the"optimization": true one. Once I run the application with a production configuration, the difference in score is significant in terms of load-time performance: 

 Lighthouse score with production build in dev environment

If I look at the opportunities list again, the number of suggestions is much lower. The biggest opportunities listed at about text compression are unused JavaScript and caching of static resources:

 Remaining opportunities for performance improvements

Angular Pre-Rendering and Server-Side Rendering 

Angular is a Single Page Application (SPA) framework. By default, the lifecycle of the app is such that upon a request from a client, the server that hosts the application serves down an HTML file that includes all the necessary script and style references. However, it is empty of body content. Once the scripts and styles are requested and served, the application is bootstrapped and populated with content based on the JavaScript logic for the given application. Angular provides two mechanisms to improve this lifecycle by serving actual content in the initial HTML document. To do this, the JavaScript logic of the application needs to be executed prior to serving the document. The ways to do it: 

  • Either at build time (pre-rendering) - for pages with mostly static content. 
  • Or at run time on the server (server-side rendering) - for pages with more dynamic content that need to deliver up-to-date content on every request. 

I have enabled server-side rendering for the Angular example app and I’m using the express engine to enable text compression and static resource caching. This is done by adding the following to my express server logic:

export const app = (lang: string) => {
  // server scaffolded by [ng add @nguniversal/express-engine]
...
  // enable compression [npm install compression]
  const compression = require('compression');
  server.use(compression());
...
  // Serve static files from /browser with 1y caching
  server.get('*.*', express.static(distFolder, {
    maxAge: '1y'
  }));
...
}

I will serve the app with server-side rendering and run the lighthouse test again. The initial load improved even further, bringing the first contentful paint to under a second, while the speed index is reduced to 1.2 seconds.

 Lighthouse score with Angular server-side rendering

The leftover opportunities for Angular optimization are to reduce unused JavaScript and CSS.

 Leftover opportunities for performance improvements

To deal with these, I will have to do some application refactoring.

Angular Lazy-Loading 

In order to reduce the unused JavaScript, the best approach would be to create lazy-loaded routes. This will tell the Angular framework which components are not needed in the top-level module and will split the JavaScript into modules, the logic for which is loaded only when the requested route is loaded. 

The Angular example app that I’m using for this blog utilizes larger components, like the igx-grid, which are not part of the home route. I’m separating the routes using this component into a separate module. This way, I’m going to have that component loaded only after the routes using the component are loaded. After separating the modules, the routes look like this:

export const routes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'register', component: RegistrationComponent },
  { path: 'unauthorized', redirectTo: 'unauthorized/', pathMatch: 'full' },
  { path: 'unauthorized/:message', component: UnauthorizedComponent },
  { path: 'emailconfirm', component: EmailconfirmComponent },
  { path: 'strategies', loadChildren: () => import('./strategies/strategies.module').then(m => m.StrategiesModule) },
  { path: 'emailconfirm/:error', component: EmailconfirmComponent },
  { path: 'players', loadChildren: () => import('./player-section/player.module').then(m => m.PlayerModule) },
  { path: 'team', loadChildren: () => import('./team-section/team.module').then(m => m.TeamModule) },
  { path: 'notifications', loadChildren: () => import('./notifications/notifications.module').then(m => m.NotificationsModule) },
  { path: 'search/teams/:query', component: TeamResultsComponent },
  { path: 'search/players/:query', component: PlayerResultsComponent },
  { path: '**', component: HomeComponent }
];

The team.module is the one loading the grid I’m using, so the code for it looks like this:

@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    // ...
    IgxGridModule,
    // ...
    TeamComponent,
    // ...
  ]
})
export class TeamModule { }

Looking at the build, this is the result of the initial splitting:

 Separating the Angular app into lazy-loaded modules

Another thing that needs to be done to improve the performance of the Angular application is to limit the size of the CSS to the styles that are used. I’m using Ignite UI for Angular as my UI library and there’s a great how-to article on customizing and optimizing the component themes. 

Optimizing Images 

Another aspect of the Angular optimization that should be done is optimizing the images. Lighthouse check will tell you if you’re using suboptimal images by type or size. Make sure the images you load are not larger than the containers they go in and they are with optimal encoding. I now use .webp format for images. It reduces the quality a little, but, still, the application does not require the highest fidelity images. 

Images cause layout shifts after they are loaded. Hence, it’s a good idea to set width and height attributes on the images so the browser knows the dimensions before loading the images. If you’re missing aspect-ratio settings (width and height) on the images, Lighthouse will warn you. This will reduce or completely eliminate the layout shifts. 

NgOptimizedImage to Enforce Image Best Practices 

Angular exposes a directive that enforces image best practices and optimizes image loading for you. It’s called NgOptimizedImage and it is rather easy to use. All you need to do is import CommonModule if you’re still in an NgModule based Angular application, or import NgOptimizedImage in the component where you want to use it. Then, instead of using src to set the image source attribute, you use ngSrc instead.

<img ngSrc="/assets/wallpapers/strat-editor.webp" width="600" height="347" class="preview-image" alt="Strategy Editor" />

Finally, you can further specify the loading priority for each image and tell the app to lazy-load every non-critical image. If I remove the width and height attributes, then what I get running the app is:

 NgOptimizedImage enforcing best practices

And that’s it. 

Wrap Up… 

Improving the performance of Angular applications may require continuous monitoring, optimization, and best practices to ensure the app performs efficiently and reliably under various conditions. But in the end, this is how you deliver the ultimate UX, attract and retain users, maintain competitiveness, and achieve business success.

Ignite UI for Angular