How to Improve Angular App Performance?

Emily Xiong
Webtips
Published in
6 min readDec 20, 2019

For almost every Angular project, I got asked to solve this question. This is an open-ended question that does not have the right answer, more like a trial and error experiment.

This blog will show you some tools and tricks that I used to improve your app performance.

Photo by Icons8 Team on Unsplash

Before You Start…

Set a BenchMark

It is very important to measure a benchmark of the performance metrics. For example:

  • Desktop environment or mobile environment?
  • Which metrics: first contentful paint, first meaningful paint, or page load time?
  • Measured using which tool?

When I tweak our apps for better performance, I need to find out whether it would be worthy to implement those changes.

Tools

Here are some tools that you could use to help you measure and analyze the performance metrics.

Lighthouse in Chrome DevTools

About this tool: https://developers.google.com/web/tools/lighthouse

Audits Tab in Chrome DevTools
Audits Tool in Chrome DevTools

By running audits, this tool will not only give you some metrics data, but it will also provide you some starting points where the app could be improved as shown below:

Results from Running Audits Using Lighthouse
Results from Running Audits Using Lighthouse

First Paint & Page Load Time Chrome Plugin

Install: https://chrome.google.com/webstore/detail/first-paint-page-load-tim/bjkmldgdbbehjahimccnckggoofdommo?hl=en

This is a chrome plugin will also give you some metrics like first paint or content loaded time for reference:

First Paint & Page Load Time Chrome Plugin UI
First Paint & Page Load Time Chrome Plugin UI

https://www.webpagetest.org/

This site provides you metrics on the deployed site.

However, it does not help you when trying to measure things during development locally.

webpack-bundle-analyzer

This tool helps us analyzing what is included in our build.

To install:

// npm:
npm install --save-dev webpack-bundle-analyzer
// or yarn:
yarn add -D webpack-bundle-analyzer

In package.json, under scripts, we could add something similar to lines below:

"build:stats": "ng build --stats-json",
"analyze": "webpack-bundle-analyzer dist/stats.json",

Then by running yarn build:stats and yarn analyze (or npm run build:stats and npm run analyze), it will show us a graph on what the outputted files are made of.

Checklist and Common Mistakes

Make sure all files are minified

This task is probably the easiest thing to do. Angular CLI will handle all the app JS and CSS files minification. However, if you are using a third-party CSS library, make you are using the minified version; or if you manually add a third-party JS script in index.html, make sure you minify that file.

For example, if you are using tachyons:

@import "~tachyons/css/tachyons.css"; /* don't do */
@import "~tachyons/css/tachyons.min.css"; /* do */

Make sure 3rd-party library are tree-shaked

For example, if we were using 3rd-party libraries like lodash, and even we were just using one function isEmpty from this library once like:

import { isEmpty } from 'lodash';

If you run webpack-bundle-analyzer, you will notice that the entire lodash the library is included as shown below:

The entire lodash library is included in the bundled file
The entire lodash library is included in the bundled file

To fix it, change the import to:

import isEmpty from 'lodash/isEmpty';

Now the lodash gzipped size changed from 23.74 KB to 1.84 KB.

Or we could use remove lodash and use lodash-es library instead:

import { isEmpty } from 'lodash-es';

Now the gzippedlodash-es library is only 364 B.

Just make sure you run the webpack-bundle-analyzer and the 3rd-party libraries are not included as a whole.

Lazy Loading

Lazy loading means only loading files as needed, so it keeps the initial bundle size small.

You could check out this blog about how to enable lazy loading: https://blog.nrwl.io/enable-lazy-loading-in-angular-2-apps-8298916056

Server-Side Rendering

Server-side rendering is generating a static pure HTML and show it to the user before the app got fully bootstrapped.

I found it is pretty easy to add server-side rendering (Angular Universal) to Angular CLI project, I could easily, just one CLI command as shown on the Angular’s website:

ng add @nguniversal/express-engine --clientProject {project-name}

{project-name} is whatever you have in the angular.json file.

However, sometimes, I still need to tweak the app to fix some runtime errors. Here are some bugs I ran into and tips on how to fix them:

  1. The app does not run with the Ahead-of-Time (AOT) compiler.

In angular.json, under configuration.production, there is this line "aot": true, meaning in the production environment, the app is using Ahead-of-Time (AOT) compiler.

For example, I got a console error Runtime compiler is not loaded:

"Runtime compiler is not loaded" error

This is very likely caused that AOT does not support arrow function. Try to remove the arrow functions in your code to see whether it would fix it.

2. Don’t use browser-only global objects directly.

As mentioned in https://angular.io/guide/universal#working-around-the-browser-apis:

server-side applications can’t reference browser-only global objects such as window, document, navigator, or location.

For example, I got an error like window is not defined as shown below:

window is not defined" error

It is caused that I use the browser-only global objects directly in my code.

To fix this, I need to declare an injection token:

import { InjectionToken } from '@angular/core';

export const WINDOW = new InjectionToken<Window>('window');

I also need to mock up a window object specifically for server environment. I use the library mock-browser.

const MockBrowser = require('mock-browser').mocks.MockBrowser;

export function windowFactory() {
return MockBrowser.createWindow();
}

For the browser environment, I could just use window object directly.

export function windowFactory() {
return window;
}

Then I need to provide this injection token in both browser environment and server environment.

// in both app.module.ts and app.server.module.ts
@NgModule({
...
providers: [{provide: WINDOW, useFactory: windowFactory}],
bootstrap: [AppComponent]
})
export class AppModule {}

Every time I need to use window object, I need to use this injection token instead:

@Injectable()
export class JwtService {

constructor(@Inject(WINDOW) private window){}

getToken(): string {
return this.window.localStorage['jwtToken'];
}
...
}

Service Worker

Note: the service worker manages caching for application. So it does not improve the performance metrics if an application is loaded for the first (application does not have anything in the cache). It only improves performance for a repeated load of an application.

Similar to server-side rendering, I found it also pretty easy to add a service worker to an Angular CLI project, just CLI command as shown on the Angular’s website:

ng add @angular/pwa --project {project-name}

The only thing you need to do manually is to change the ngsw-config.json file. For example, if you load google fonts, you might need to add URL https://fonts.gastic.com/** to your ngsw-config.json file.

{
"index": "/index.html",
"assetGroups": [
...
{
"name": "assets",
"installMode": "lazy",
"updateMode": "prefetch",
"resources": {
...
"urls": [
"https://fonts.gstatic.com/**"
]
}
}
]
}

Change Detection

For dummy components, we could add the OnPush change detection strategy:

changeDetection: ChangeDetectionStrategy.OnPush

Here is an article that explains OnPush change detection strategy: https://netbasal.com/a-comprehensive-guide-to-angular-onpush-change-detection-strategy-5bac493074a4

Summary

Above are some things you could do to improve your Angular app performance. I found it is easier to set up things such as lazy loading, server-side rendering, and service worker, at the beginning of the project rather than at the final stage.

Let’s build faster Angular apps! Happy coding! :)

--

--