How to optimize an Angular app? First, we need to consider how we understand optimization as such. Optimization helps achieve better app performance and easier code maintenance. Optimization is not a checklist or a to-do-list, and there is no universal way to optimize your app.
Optimization is a process, and it should start at the same time as the app development. It’s really hard to optimize the application that already exists (though it’s still possible of course), so we should take care of it during the whole project.
Here's a list of the best tips to help keep your code in line with good practices and make your Angular application faster. Enjoy and optimize!
Trivial, but really important - each part of the application should be minified before deploying to the production stage. If you have ever used Webpack, you probably know plugins such as UglifyJS, MinifyCSS, etc. They remove every whitespace and every function that is never executed. Moreover, they change functions' and variables' names to shorter ones that make the code almost unreadable, but the size of the compiled bundle is smaller.
Fortunately, with Angular, we don’t have to remember to add Webpack scripts to minify the code. All we have to do is make a bundle using
ng build --prod
command. It’s just good to know when and how it happens.Each Angular component has its own Change Detector that is responsible for detecting when the component data has been changed, and automatically re-render the view to reflect the changes. Change Detector is called whenever DOM emits an event (button click, form submit, mouseover etc.), an HTTP request is executed, or there’s an asynchronous interaction such as setTimeout or setInterval. On every single event, the Change Detector will be triggered in the whole application tree (from the top to the bottom, starting with the root component).
The numbers show the order of checking the changes by the Change Detectors after the event in the Component in the left bottom corner. To change Detection Strategy for the component, the changeDetection in Component declaration should be set to ChangeDetectionStrategy.OnPush as below:
@Component({
...
changeDetection: ChangeDetectionStrategy.OnPush
})
After that, the Change Detectors will work by comparing references to the inputs of the component. Inputs in this component are immutable and if values in these inputs have not changed, change detection skips the whole subtree of Change Detectors, as depicted below:
Change detector for a component with OnPush Strategy will be triggered only when a value in @Input() has been changed, an event has been emitted by a template, an event has been triggered by Observable in this component or
this.changeDetector.markForCheck()
has been called.class foo {
private _bar: boolean = false;
get bar(): boolean {
return this._bar;
}
}
get
used in the view is nothing more than a function call. A better idea is to use pure pipes which will always return the same output, no matter how many times they will receive the same input. If the Change Detector reaches this view and pure in the pipe is set to true (default), the Change Detector will not check if that value has been changed because it knows that it will return exactly the same value.<div>Time: {{ time | async }}</div>
The async pipe allows subscribing to Observable directly from the template.
With async pipe, there's no need to bother about unsubscribing. What’s more, Change Detector will check if the value was changed only when Observable had changed itself.
When using RxJS, it’s really important not to forget about unsubscribing, otherwise there’s a risk to get memory leaks in our application. There are a few methods to unsubscribe a Subscription, but choosing the best one is probably a subject for another blog post - for now, just remember to do it. Here are the methods →
1st method:
let sub1: Subscription;
let sub2: Subscription;
ngOnInit() {
this.sub1 = this.service.Subject1.subscribe(() => {});
this.sub2 = this.service.Subject2.subscribe(() => {});
}
ngOnDestroy() {
if (this.sub1) {
this.sub1.unsubscribe();
}
if (this.sub2) {
this.sub2.unsubscribe();
}
}
2nd method:
let subs: Subscription[] = [];
ngOnInit() {
this.subs.push(this.service.Subject1.subscribe(() => {}));
this.subs.push(this.service.Subject2.subscribe(() => {}));
}
ngOnDestroy() {
subs.forEach(sub => sub.unsubscribe());
}
3rd method:
private subscriptions = new Subscription();
ngOnInit() {
this.subscriptions.add(this.service.Subject1.subscribe(() => {}));
this.subscriptions.add(this.service.Subject2.subscribe(() => {}));
}
ngOnDestroy() {
this.subscriptions.unsubscribe();
}
4th method:
ngUnsubscribe = new Subject();
ngOnInit() {
this.service.Subject1.pipe(takeUntil(this.ngUnsubscribe))
.subscribe(() => {});
this.service.Subject2.pipe(takeUntil(this.ngUnsubscribe))
.subscribe(() => {});
}
ngOnDestroy() {
this.ngUnsubscribe.next();
this.ngUnsubscribe.complete();
}
*ngFor="let item of items; trackBy: trackByFn"
trackBy is a parameter that accepts a function that should return a unique value of each item of the list. Without using trackBy function, *ngFor will re-render each and every element of the list every time that list changes (each element will be removed from DOM and rendered once again). With trackBy function, only the values that have been changed will be re-rendered or deleted.
If you find out that your bundle’s size is too big, you can check what exactly is in the bundle and decide whether you need all the external libraries or not. You can use e.g. Webpack Bundle Analyzer. All you need to do is just provide a simple configuration based on your application and your needs, and analyze the output graph.
After generating a graph via Webpack Bundle Analyzer you probably know that you shouldn’t add the whole library to your project if you use only a small feature from it. Try to code the same function that you import from the library on your own. For example, a concat method from Lodash
import { concat } from 'lodash'
concat([1], 2, [3], [[4]])
is nothing more than
[1].concat(2, [3], [[4]])
The more code you write on your own, the bigger control and understanding of how it works you have. Moreover, it’s easier to debug and maintain the code without unnecessary external libraries.
However, if you need to import something, choose modular library and
import { chunk } from “lodash”
instead of
import * from “lodash”
No matter if you use SASS, LESS or pure CSS (is there anybody who still uses pure CSS? 😉) try to write as many styles per component as you can, instead of global ones.
In the files with global styles declare variables with colors, fonts, reusable components, like buttons, dropdowns, form inputs, but keep everything else divided into components.
Properties will be loaded only if the component is used in the rendered page, so it means better performance for the application. For you, as a front-end developer, it means that the SCSS is scoped and simplified, so you will be probably more productive during development.
You can imagine that the application is a tree with green and brown leaves that refer respectively to used and unused parts of the code. When you try to shake that tree, the brown leaves (dead code) will fall down.
When using
ng build --prod
to build, the Angular CLI will compile and bundle all components into the application.When using
ng build --prod --build-optimizer
to build, all of the unused components will be omitted.--build-optimizer
activates tree shaking (a term commonly used for dead-code elimination in JavaScript context) on Webpack.Unfortunately, this can cause some bugs, so be really careful while doing that and rather than excluding dead code, try including living code.
While binding properties between components check if:
[Source: Angular.io]
If if the property covers all of these criteria omit the brackets and use
label="Save"
instead of
[label]=”’Save’”
This way the Change Detector will not check it while running in the component.
10. Dependencies between components
The next thing to consider are dependencies between components in the application. Are all of them really necessary? Do you want and need them all? Try to check it by generating and analyzing dependencies graph (for example NGD: Angular Dependencies Graph).
It can be also helpful when you start working on an already existing project and want to have an overview of the architecture of the application.
Remember to use the appropriate animations, depending on what you want to achieve. CSS animations are perfect for smaller, self-contained states for UI elements, e.g. a sidebar with menu appearing from the side, a tooltip showing on hover, while JavaScript gives more control over animations, e.g. possibility to stop, slow down, reverse or pause the animation.
CSS animations are smaller so use them every time you can, and only when they are not enough for you, choose JS ones.
A good practice is to use a CSS animation to make a global loader for the application - then, even if the JavaScript of the application doesn’t load properly, the user can see a nice, animated loader.
Lazy loading loads only those modules and components that are necessary. If the user is not an admin, there is no need to load the entire AdminModule, and if the user is not signed into the application and uses it anonymously, there is no need to load the AccountModule, which allows to view and edit the profile.
const routes: Routes = [
{ path: 'account',
loadChildren: () => import('./account/account.module')
.then(m => m.AccountModule) },
{ path: admin,
loadChildren: () => import('./admin/admin.module')
.then(m => m.AdminModule) }
];
What's more, you can try lazy load for a bigger amount of images or longer content - load it on scroll.
Move as much application logic as possible to the backend. When the logic of the application relies little on the choices of the user, there is no need to perform huge calculations on the frontend side.