Cải thiện hiệu năng trong ứng dụng Angular

·

7 min read

Bài này sẽ bàn về các phương pháp giúp tăng hiệu năng trong ứng dụng angular

1. Lazy load

Một ứng dụng angular có nhiều tính năng và thường thì ta sẽ chia mỗi tính năng thành 1 module. Khi tải ứng dụng, nếu không sử dụng lazy load thì angular sẽ tải tất cả các module này 1 lúc, vấn đề ở đây là có nhiều trang nằm trên các module khác mà ta chưa cần hiển thị nhưng angular vẫn tải lên, điều này là thừa và gây vấn đề hiệu suất, lazy load sẽ giải quyết vấn đề này. Ý nghĩa của việc lazy load là chỉ tải khi dùng đến, tức là khi ta click vào 1 link nào đấy, ta sẽ di chuyển sang trang khác, lúc này module chứa trang này mới được tải lên.

VD1: TH không dùng lazy load const routes: Routes = [ {path: 'dashboard', component: 'DashboardModule'}, {path: 'admin', component: 'AdminModule'}, {path: 'purchase', component: 'PurchaseModule'}, {path: '**', redirectTo: 'dashboard'} ]

=> khi ứng dụng được tải lên thì DashboardModule, AdminModule, PurchaseModule được tải luôn, nếu có nhiều module hơn thì tải sẽ lâu

VD2: TH dùng lazy load const routes: Routes = [ {path: 'dashboard', loadChildren: () => import('./admin/dashboard.module').then(m => m.DashboardModule)}, {path: 'admin', loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule)}, {path: 'purchase', loadChildren: () => import('./admin/purchase.module').then(m => m.PurchaseModule)}, {path: '**', redirectTo: 'dashboard'} ]

=> khi click vào link dashboard => DashboardModule mới được tải

2. trackBy

Khi danh sách hiển thị trên màn hình được update, lúc này angular sẽ render lại toàn bộ danh sách mới lên html, nếu như các record bán đầu không thay đổi gì mà vẫn phải render lại là không cần thiết và làm app chậm hơn, sử dụng trackBy sẽ giải quyết vấn đề này

VD1: TH không sử dụng trackBy @Component({ selector: 'the-fruits', template: <ul> <li *ngFor="let fruit of fruits">{{ fruit.name }}</li> </ul> <button (click)="addFruit()">Add fruit</button> }) export class FruitsComponent { fruits = [ { id: 1, name: 'Banana' }, { id: 2, name: 'Apple' }, { id: 3, name: 'Pineapple' }, { id: 4, name: 'Mango' } ]; addFruit() { this.fruits = [ ...this.fruits, { id: 5, name: 'Peach' } ]; } }

=> khi click button Add fruit, fruits sẽ có thêm 1 record ({ id: 5, name: 'Peach' }), lúc này html sẽ render lại tất cả các records trên fruits

VD2: TH dùng trackBy @Component({ template: <ul> <li *ngFor="let fruit of fruits; trackBy: trackUsingId"> {{ fruit.name }} </li> </ul> <button (click)="addFruit()">Add fruit</button> }) export class FruitsComponent { fruits = [ ... ]; ... trackUsingId(index, fruit){ return fruit.id; } }

=> khi click button Add fruit, fruits sẽ có thêm 1 record ({ id: 5, name: 'Peach' }) => html sẽ chỉ render lại record này

Chú ý: trackBy sẽ hữu ích trong TH phải update lại list để hiển thị

3. Sử dụng AOT thay vì JIT để biên dịch

Trong ứng dụng angular angular, trước khi trình duyệt có thể hiển thị ứng dụng, các component, template phải được trình biên dịch Angular chuyển đổi sang JavaScript thực thi. Với AOT thì trình duyệt sẽ tải xuống mã đã được biên dịch trước, nên nó có thể hiển thị luôn. Với JIT thì khi trình duyệt truy cập ứng dụng, nó sẽ tải cả trình biên dịch về (@angular/compile) để vừa chạy vừa biên dịch nên việc hiển thị sẽ chậm hơn.

JIT:

  • Tất cả các typescript sẽ được biên dịch sang javascript
  • Trình duyệt tải tất cả các assets (js, css, images...)
  • Angular khởi tạo ứng dụng (bootstrap)
  • Mỗi component sẽ generate javascript và template trên trình duyệt
  • Render

AoT:

  • Tất cả các angular template được biên dịch sang typescript bằng angular compiler, sau đó tất cả typescript được compile sang javascript
  • Trình duyệt tải tất cả assets
  • Angular khởi tạo ứng dụng
  • Render

Do đó, quá trình build của AoT chắc chắn sẽ lâu hơn, nhưng render lại nhanh hơn vì tất cả đã được biên dịch sẵn và chỉ chờ để đưa lên.

Cách dùng: ng build --prod --aot hoặc config trong angular.json: projects -tên app - architect - build - options - aot = true và chạy ng b --prod

4. Sử dụng pipe thay vì dùng function trong html

Nguyên nhân: pipe sẽ chỉ kích hoạt lại khi đối số đầu vào thay đổi tham chiếu (kiểu đối tượng, array), còn với method nó sẽ thực thi mỗi khi data thay đổi.

VD:

Component: 

import { Component } from '@angular/core';
@Component({
  selector: 'app-user',
  templateUrl: './user.component.html',
  styleUrls: ['./user.component.scss'],
})

export class UserComponent {
  user = {
    id: 1001,
    firstName: 'Jane',
    lastName: 'Doe',
    title: 'Miss',
  };

  userName(): string {
    console.log('Method called');
    return `${this.user.title}. ${this.user.firstName} ${this.user.lastName}`;
  }

  changeName() {
    this.user.firstName = 'Nguyen ';
    this.user.lastName = 'Phuong';
  }
}
Pipe:

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({ name: 'displayName' })
export class DisplayNamePipe implements PipeTransform {
  transform(user: any): string {
    console.log('Pipe called');
    return `${user.title} ${user.firstName} ${user.lastName}`;
  }
}
Template:

<p>{{ userName() }}</p>
<p>{{ user | displayName }}</p>
<p>{{ user.id }}</p>
<p>{{ user.firstName }}</p>
<p>{{ user.lastName }}</p>
<p>{{ user.title }}</p>

Trên console hiển thị như sau: Method called Pipe called Method called Method called Method called

Ngay khi hiển thị màn hình thì việc sử dụng method userName() sẽ call 4 lần, còn nếu sử dụng Pipe chỉ call 1 lần vì transform chỉ được trigger lại khi tham chiếu đầu vào thay đổi, cần lưu ý khi giá trị đầu vào có kiểu nguyên thủy, khi giá trị thay đổi thì nó sẽ không được cập nhật trên template.

5. Change detection

Angular bao gồm một cây các component, mỗi component có trình cung cấp ChangeDetectorRef của riêng nó, chịu trách nhiệm xử lý cập nhật liên kết của component đó. Khi ứng dụng được khởi động, ban đầu nó tạo ApplicationContext(root) và trigger phương thức tick() trên AppComponent, phương thức này bắt đầu chạy phát hiện thay đổi trên từng component từ trên cùng (AppComponent) của cây.

Theo mặc định: khi phát hiện thay đổi kích hoạt ở bất kỳ component nào, nó sẽ bắt đầu kích hoạt từ component gốc (AppComponent) để xem có cập nhật view hay không. Phát hiện thay đổi xảy ra khi:

  • DOM events (mouse, key, …)
  • Requests api
  • Timers (setTimers(), setInterval())

Quá trình phát hiện thay đổi này xảy ra đối với tất cả các component. Nó đánh giá tất cả các ràng buộc của một component, bất kể ràng buộc đó có bị thay đổi hay không, điều này ảnh hưởng rất lớn đến hiệu suất. Giả sử một cây có khoảng 50 cấp component và một số component đã kích hoạt phát hiện thay đổi, cuối cùng sẽ kích hoạt phát hiện thay đổi ở tất cả các component từ trên xuống dưới, điều này có nghĩa những component không cần chạy phát hiện thay đổi cũng sẽ phải chạy => vừa thừa vừa ảnh hưởng lớn đến hiệu năng. Để khắc phục vấn đề này, ta sẽ dùng Change Detection OnPush.

Chỉ cần thêm changeDetection: ChangeDetectionStrategy.OnPush vào bên trong @Component @Component({ templateUrl: ..., changeDetection: ChangeDetectionStrategy.OnPush })

Khi này, từ component này chở xuống, các component con sẽ chỉ chạy phát hiện thay đổi khi:

  • Kích hoạt lại bằng cách thủ công: gọi method detectChanges() bằng đối tượng ChangeDetectorRef
  • Gọi phương thức ApplicationRef.tick() để kích hoạt lại toàn bộ phát hiện thay đổi của cả app
  • Tham chiếu đầu vào (@input) thay đổi
  • Chính component này hoặc 1 trong các component con của nó kích hoạt 1 sự kiện
  • Khi async pipe emit data
  • Dùng phương thức markForCheck, chú ý khi dùng phương thức này thì app chạy phát hiện thay đổi lần tiếp theo nó sẽ chạy vào component này nhưng chỉ 1 lần

myMethod() { this.cd.markForCheck(); }

Các sự kiện sau KHÔNG kích hoạt phát hiện thay đổi

  • Data đầu vào(@Input) chỉ thay đổi giá trị mà không phải thay đổi tham chiếu
  • setTimeout
  • setInterval
  • Promise.resolve().then(), Promise.reject().then()
  • Requests api

6. Sử dụng detach

Khi muốn tách rời 1 component khỏi phát hiện thay đổi, tức là khi app chạy phát hiện thay đổi sẽ không chạy từ component này xuống con của nó, ta dùng detach.

constructor(cd: ChangeDetectorRef) { cd.detach(); }

Nếu muốn đưa component này lại cho mỗi lần chạy phát hiện thay đổi đều chạy qua nó thì dùng

mySpecificMethod() { this.cd.reattach(); }

7. Sử dụng NgZone chạy Angular outside

constructor(private zone: NgZone) { this.zone.runOutsideAngular(() => { // code }); }

Những thay đổi giá trị property được viết trong scope của hàm runOutsideAngular() sẽ không kích hoạt phát hiện thay đổi, thích hợp cho Th như lấy tín hiệu thông báo, update theo thời gian thực như chat, comment

Ngoài ra còn 1 số phương pháp khác nữa là: tối ưu html, css, sử dụng rxjs, config phía server để cache file tĩnh như html, css, assets, js, dùng service worker ...