Skip to content

Instantly share code, notes, and snippets.

@jziggas
Last active November 12, 2021 21:03
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jziggas/d2f10f888304fd8fc8af6d023da485e2 to your computer and use it in GitHub Desktop.
Save jziggas/d2f10f888304fd8fc8af6d023da485e2 to your computer and use it in GitHub Desktop.

Angular Peformance Guidelines

Runtime strategies

These are the strategies we can use while the application is running in the browser.

Avoid function calls in view templates

Angular's zone can perform change detection every time there is a user interaction on the document, such as during a click, mouse move, timer tick, or http call. Therefore function calls in the template get invoked and recalculated each time this happens. This can add a lot of unnecessary overhead during runtime especially with complex logic or multiple function calls

Bad:

<div *ngIf="isFooBar(user)"></div>
isFoorBar() {
    return user.foo && user.bar;
}

Good:

<div *ngIf="user.$isFooBar"></div>
ngOnInit() {
    user.$isFooBar = user.foo && user.bar;
}

By calculating a property such as user.$isFoorBar once during component initialization you can avoid hundreds of recalculations for a value that would normally remain unchanged during the component lifecycle.

Pipes

Bad: Impure pipes

Good: Pure pipes

Pipes are pure by default, meaning they maintain no internal state and given the same input will always return the same output. Do everything possible to never use impure pipes because they behave similarly to a function call within a view template (they will be called during each change detection cycle). But pure pipes are only called when the input changes. As an alternative if some state needs to be managed you can create a pipe which returns an Observable and pass the Observable into the async pipe, e.g. <div>{{foo | somePipe | async}}</div>.

Use trackBy with ngFor

Manipulating the DOM can be very expensive and can add up quickly when rendering long lists of items. When repeating over a list of complex objects using ngFor, Angular checks for a change in reference to the list. Therefore when a subscription updates a list of items Angular will think the whole list has changed even though it may be that only one item in the list needs to be updated. Using a trackBy function tells Angular if an individual item is different or the same. A TrackBy (track-by.ts) helper class can be implemented for the most common trackBy functions, and if extending from a BaseComponent (base.component.ts) you get it for free.

Bad:

<div *ngFor="let item of longListOfItems">
  <div>{{item.name}}</div>
</div>

Good:

<div *ngFor="let item of longListOfItems; trackBy: trackBy.item">
  <div>{{item.name}}</div>
</div>
class SomeComponent extends BaseComponent {}

// or

class SomeComponent extends ListComponent {}

// or

class SomeComponent {
    trackBy: TrackBy = new TrackBy();
}

Caching

A CacheService saves items that are commonly used across the entire application, such as accounts or user, to reduce the number of HTTP requests required per component to load these items. ShareReplay() can be utilized to accomplish this.

Other methods of caching that could be used include:

  • Saving a DOM node to a variable to prevent having to recreate one
  • Saving the contents of an HTTP request when it is fetched for the duration of a component lifecycle or even the application

OnPush Change Detection Strategy

Nearly every component should use the OnPush change detection strategy. In a nutshell what this generally means is that each component will only perform a change detection cycle if one of its inputs changes or if we tell it to.

Subscriptions

RxJS Subscriptions need to eventually be unsubscribed from when a during a component's OnDestroy lifecycle hook, otherwise memory leaks will occur due to the Subscription never having been cleaned up and its own lifecycle ended. This is generally taken care of already when extending from our BaseComponent or ListComponent because we save the subscriptions that automatically get cleaned up later. But otherwise you will need to do the cleanup on your own.

class SomeComponent extends BaseComponent {
    constructor(private cache: CacheService) {}
    ngOnInit() {
        // this.subscriptions is instantiated via BaseComponent
        this.subscriptions.users = this.cache.users.subscribe((user) => {
            ...
        });
    }
}
class SomeOtherComponent {
    // We need to manage our own subscriptions without extending from BaseComponent or ListComponent
    destroyed$ = new Subject();
    constructor(private cache: CacheService) {}
    ngOnInit() {
        this.cache.users
            .pipe(takeUntil(this.destroyed$)).subscribe((user) => {
            ...
        });
    }

    ngOnDestroy() {
        this.destroyed$.next(true);
        this.destroyed$.complete();
    }
}

Compilation and build-time strategies

These are strategies we can use when compiling our source files into production-ready files.

Lazy Loading Modules

The application is organized into modules based on some of the most common routes so that only the code that is necessary to view that route is loaded instead of the entire application. Some examples include Administration, Search, or Home. Components that are commonly needed across the entire application are found in a SharedModule.

Concatenation and Minification

We use Webpack to concatenate our source files into their respective modules, and we use UglifyJS to minify the code into the smallest possible files to make loading each one as fast as possible. gzip is also used to compress the files across network requests.

Removal of whitespaces

https://medium.com/@amcdnl/performance-tip-preserving-whitespace-623378a38c32

This might not make any significant differences for individual components but will reduce our bundle size and might improve rendering speed.

Production mode

Angular by default runs in debug mode, which adds in some assertion checks and runs ChangeDetection twice each time to ensure there are no unexpected changes to values. So in our app.ts you will find:

import {enableProdMode} from '@angular/core';
if (environment.production) {
    enableProdMode();
}

Ahead of Time compilation (AOT)

An Angular application consists mainly of components and their HTML templates. Because the components and templates provided by Angular cannot be understood by the browser directly, Angular applications require a compilation process before they can run in a browser.

The Angular Ahead-of-Time (AOT) compiler converts your Angular HTML and TypeScript code into efficient JavaScript code during the build phase before the browser downloads and runs that code. Compiling your application during the build process provides a faster rendering in the browser.

Service Workers

A service worker is a script that runs in the web browser and manages caching for an application.

Application Shell

App shell is a way to render a portion of your application via a route at build time. It can improve the user experience by quickly launching a static rendered page (a skeleton common to all pages) while the browser downloads the full client version and switches to it automatically after the code loads.

This gives users a meaningful first paint of your application that appears quickly because the browser can simply render the HTML and CSS without the need to initialize any JavaScript.

Server Side Compilation

Angular's server side compilation.

Performance troubleshooting

Chrome profiler

The Chrome performance profiler will be one of the most useful tools in your arsenal. It works by recording everything that is run in the browser and showing the call stacks in your source code over time. These recordings can also be saved for later analysis. Before reading blog posts there is a lot of useful information provided by Google on to Get Started with Analyzing Runtime Performance.

Angular debug tools

Enabled Angular debug tools that are accessible via your browser's developer console.

Usage:

  1. Open developer console (e.g. in Chrome Ctrl + Shift + j)
  2. Type ng. (usually the console will show auto-complete suggestion)
  3. Try the change detection profiler ng.profiler.timeChangeDetection() then hit Enter.

Augury

Augury is a Chrome extension for debugging Angular applications. It won't necessarily help with performance problems but it can help by providing insight into application structure and how components and services interact with each other.

Efforts

Low effort, medium reward:

Low effort, low reward:

Medium effort, low reward:

  • Detach the change detector in more components and manually control when they get updated.

High effort, high reward:

  • Remove real-time live updates in favor of a notification based (toast) mechanism to inform the user when updates are available. The user would click a call to action to view the updates. This would allow thousands of rows to be viewed while only updating values when the user chooses to.
  • Implement virtual scrolling for all tables. This would allow thousands of rows to be loaded while only the ones in the viewport are being rendered instead of all of them.

Benchmarking

The tool https://www.npmjs.com/package/@thi.ng/bench can be leveraged to execute benchmarks. It might be easier to write benchmarks inside unit tests, so you can make changes and re-run the benchmark, although one should be able to write benchmarks within the application as well.

When running within the Jasmine and Angular frameworks they mock out the clock and Date calls, so you need to run the benchmark function outside of the fakeAsync wrapper.

In some cases it may be easiest to write two duplicate components and run benchmarks against both the original and updated component, so you can determine if code changes are making a difference.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment