These are the strategies we can use while the application is running in the browser.
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.
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>
.
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();
}
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
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.
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();
}
}
These are strategies we can use when compiling our source files into production-ready files.
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.
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.
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.
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();
}
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.
A service worker is a script that runs in the web browser and manages caching for an application.
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.
Angular's server side compilation.
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.
Enabled Angular debug tools that are accessible via your browser's developer console.
Usage:
- Open developer console (e.g. in Chrome Ctrl + Shift + j)
- Type
ng.
(usually the console will show auto-complete suggestion) - Try the change detection profiler
ng.profiler.timeChangeDetection()
then hit Enter.
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.
Low effort, medium reward:
- ngZoneEventCoalescing - As of Angular 9, this is a configuration option that gets passed into the
bootstrapModule
callback of the app that reduces the number of change detection cycles that occur during event propogation. This is app-level configuration change groups change events together when they happened from event propagating up throught the DOM hierarchy. (https://netbasal.com/reduce-change-detection-cycles-with-event-coalescing-in-angular-c4037199859f) (angular/angular#30533)
Low effort, low reward:
- Reduce the number of dom nodes that need to be rendered by e.g. analyzing ngFor code blocks, removing unnecessary divs.
- use the
preserveWhitespaces
component configuration in more components. This is more about reducing file size and speed of component creation than it is for change detection (https://medium.com/@amcdnl/performance-tip-preserving-whitespace-623378a38c32) - memoization of values returned by pipes. this comes at the cost of memory and has marginal benefit from initial testing (https://blog.bitsrc.io/angular-performance-optimizing-expression-re-evaluation-with-pure-pipes-ff8df36ed478)
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.
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.