Tối ưu hoá khả năng phát hiện thay đổi của Angular

Triển khai tính năng phát hiện thay đổi nhanh hơn để mang lại trải nghiệm tốt hơn cho người dùng.

Angular chạy cơ chế phát hiện thay đổi định kỳ để các thay đổi đối với mô hình dữ liệu được phản ánh trong chế độ xem của ứng dụng. Bạn có thể kích hoạt tính năng phát hiện thay đổi theo cách thủ công hoặc thông qua một sự kiện không đồng bộ (ví dụ: tương tác của người dùng hoặc hoàn tất XHR).

Tính năng phát hiện thay đổi là một công cụ mạnh mẽ, nhưng nếu chạy thường xuyên, tính năng này có thể kích hoạt nhiều phép tính và chặn luồng trình duyệt chính.

Trong bài đăng này, bạn sẽ tìm hiểu cách kiểm soát và tối ưu hoá cơ chế phát hiện thay đổi bằng cách bỏ qua một số phần của ứng dụng và chỉ chạy cơ chế phát hiện thay đổi khi cần.

Bên trong tính năng phát hiện thay đổi của Angular

Để hiểu cách hoạt động của tính năng phát hiện thay đổi của Angular, hãy xem một ứng dụng mẫu!

Bạn có thể tìm thấy mã của ứng dụng trong kho lưu trữ GitHub này.

Ứng dụng này liệt kê nhân viên của hai phòng ban trong một công ty – bán hàng và R&D – và có hai thành phần:

  • AppComponent là thành phần gốc của ứng dụng và
  • Hai thực thể của EmployeeListComponent, một thực thể cho hoạt động bán hàng và một thực thể cho hoạt động nghiên cứu và phát triển.

Ứng dụng mẫu

Bạn có thể thấy hai thực thể của EmployeeListComponent trong mẫu cho AppComponent:

<app-employee-list
  [data]="salesList"
  department="Sales"
  (add)="add(salesList, $event)"
  (remove)="remove(salesList, $event)"
></app-employee-list>

<app-employee-list
  [data]="rndList"
  department="R&D"
  (add)="add(rndList, $event)"
  (remove)="remove(rndList, $event)"
></app-employee-list>

Mỗi nhân viên có một tên và một giá trị dạng số. Ứng dụng sẽ truyền giá trị số của nhân viên đến một phép tính kinh doanh và hiển thị kết quả trên màn hình.

Bây giờ, hãy xem EmployeeListComponent:

const fibonacci = (num: number): number => {
  if (num === 1 || num === 2) {
    return 1;
  }
  return fibonacci(num - 1) + fibonacci(num - 2);
};

@Component(...)
export class EmployeeListComponent {
  @Input() data: EmployeeData[];
  @Input() department: string;
  @Output() remove = new EventEmitter<EmployeeData>();
  @Output() add = new EventEmitter<string>();

  label: string;

  handleKey(event: any) {
    if (event.keyCode === 13) {
      this.add.emit(this.label);
      this.label = '';
    }
  }

  calculate(num: number) {
    return fibonacci(num);
  }
}

EmployeeListComponent chấp nhận danh sách nhân viên và tên phòng ban làm dữ liệu đầu vào. Khi người dùng cố gắng xoá hoặc thêm nhân viên, thành phần này sẽ kích hoạt một đầu ra tương ứng. Thành phần này cũng xác định phương thức calculate để triển khai tính toán kinh doanh.

Sau đây là mẫu cho EmployeeListComponent:

<h1 title="Department">{{ department }}</h1>
<mat-form-field>
  <input placeholder="Enter name here" matInput type="text" [(ngModel)]="label" (keydown)="handleKey($event)">
</mat-form-field>
<mat-list>
  <mat-list-item *ngFor="let item of data">
    <h3 matLine title="Name">
      {{ item.label }}
    </h3>
    <md-chip title="Score" class="mat-chip mat-primary mat-chip-selected" color="primary" selected="true">
      {{ calculate(item.num) }}
    </md-chip>
  </mat-list-item>
</mat-list>

Mã này lặp lại trên tất cả nhân viên trong danh sách và hiển thị một mục danh sách cho mỗi nhân viên. Tệp này cũng bao gồm một lệnh ngModel để liên kết dữ liệu hai chiều giữa dữ liệu đầu vào và thuộc tính label được khai báo trong EmployeeListComponent.

Với hai thực thể của EmployeeListComponent, ứng dụng sẽ tạo thành cây thành phần sau:

Cây thành phần

AppComponent là thành phần gốc của ứng dụng. Các thành phần con của nó là hai thực thể của EmployeeListComponent. Mỗi thực thể có một danh sách các mục (E1, E2, v.v.) đại diện cho từng nhân viên trong bộ phận.

Khi người dùng bắt đầu nhập tên của một nhân viên mới vào hộp nhập trong EmployeeListComponent, Angular sẽ kích hoạt tính năng phát hiện thay đổi cho toàn bộ cây thành phần bắt đầu từ AppComponent. Điều này có nghĩa là trong khi người dùng nhập dữ liệu văn bản, Angular sẽ liên tục tính toán lại các giá trị số liên kết với từng nhân viên để xác minh rằng các giá trị này không thay đổi kể từ lần kiểm tra gần đây nhất.

Để xem tốc độ chậm đến mức nào, hãy mở phiên bản chưa được tối ưu hoá của dự án trên StackBlitz rồi thử nhập tên nhân viên.

Bạn có thể xác minh rằng sự chậm trễ đến từ hàm fibonacci bằng cách thiết lập dự án mẫu và mở thẻ Hiệu suất của Công cụ của Chrome cho nhà phát triển.

  1. Nhấn tổ hợp phím `Ctrl+Shift+J` (hoặc `Command+Option+J` trên máy Mac) để mở DevTools.
  2. Nhấp vào thẻ Hiệu suất.

Bây giờ, hãy nhấp vào biểu tượng Record (Ghi) (ở góc trên cùng bên trái của bảng điều khiển Performance (Hiệu suất)) rồi bắt đầu nhập vào một trong các hộp văn bản trong ứng dụng. Sau vài giây, hãy nhấp lại vào biểu tượng Record (Ghi) để dừng ghi. Sau khi Công cụ của Chrome cho nhà phát triển xử lý tất cả dữ liệu phân tích tài nguyên đã thu thập, bạn sẽ thấy nội dung như sau:

Phân tích hiệu suất

Nếu có nhiều nhân viên trong danh sách, quy trình này có thể chặn luồng giao diện người dùng của trình duyệt và gây ra hiện tượng sụt khung hình, dẫn đến trải nghiệm người dùng kém.

Bỏ qua các cây con thành phần

Khi người dùng nhập dữ liệu văn bản cho EmployeeListComponent bán hàng, bạn biết rằng dữ liệu trong bộ phận R&D (Nghiên cứu và phát triển) không thay đổi. Vì vậy, không có lý do gì để chạy tính năng phát hiện thay đổi trên thành phần đó. Để đảm bảo thực thể R&D không kích hoạt tính năng phát hiện thay đổi, hãy đặt changeDetectionStrategy của EmployeeListComponent thành OnPush:

import { ChangeDetectionStrategy, ... } from '@angular/core';

@Component({
  selector: 'app-employee-list',
  template: `...`,
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrls: ['employee-list.component.css']
})
export class EmployeeListComponent {...}

Giờ đây, khi người dùng nhập văn bản, tính năng phát hiện thay đổi chỉ được kích hoạt cho bộ phận tương ứng:

Phát hiện thay đổi trong cây con thành phần

Bạn có thể tìm thấy cách tối ưu hoá này được áp dụng cho ứng dụng gốc tại đây.

Bạn có thể đọc thêm về chiến lược phát hiện thay đổi OnPush trong tài liệu chính thức về Angular.

Để xem hiệu quả của việc tối ưu hoá này, hãy nhập một nhân viên mới trong ứng dụng trên StackBlitz.

Sử dụng các ống thuần tuý

Mặc dù chiến lược phát hiện thay đổi cho EmployeeListComponent hiện được đặt thành OnPush, nhưng Angular vẫn tính toán lại giá trị số cho tất cả nhân viên trong một bộ phận khi người dùng nhập dữ liệu văn bản tương ứng.

Để cải thiện hành vi này, bạn có thể tận dụng ống dẫn thuần tuý. Cả ống thuần tuý và không thuần tuý đều chấp nhận dữ liệu đầu vào và trả về kết quả có thể dùng trong mẫu. Sự khác biệt giữa hai loại này là một pipe thuần tuý sẽ chỉ tính toán lại kết quả nếu nhận được dữ liệu đầu vào khác với lệnh gọi trước đó.

Hãy nhớ rằng ứng dụng sẽ tính toán một giá trị để hiển thị dựa trên giá trị số của nhân viên, gọi phương thức calculate được xác định trong EmployeeListComponent. Nếu bạn di chuyển phép tính sang một ống thuần tuý, Angular sẽ chỉ tính toán lại biểu thức của ống khi các đối số của biểu thức đó thay đổi. Khung này sẽ xác định xem các đối số của pipe có thay đổi hay không bằng cách thực hiện kiểm tra tham chiếu. Điều này có nghĩa là Angular sẽ không thực hiện bất kỳ phép tính lại nào trừ phi giá trị số cho một nhân viên được cập nhật.

Dưới đây là cách di chuyển phép tính kinh doanh sang một ống có tên là CalculatePipe:

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

const fibonacci = (num: number): number => {
  if (num === 1 || num === 2) {
    return 1;
  }
  return fibonacci(num - 1) + fibonacci(num - 2);
};

@Pipe({
  name: 'calculate'
})
export class CalculatePipe implements PipeTransform {
  transform(val: number) {
    return fibonacci(val);
  }
}

Phương thức transform của pipe gọi hàm fibonacci. Lưu ý rằng ống này là thuần tuý. Angular sẽ xem xét tất cả các ống dẫn thuần tuý trừ phi bạn chỉ định khác.

Cuối cùng, hãy cập nhật biểu thức bên trong mẫu cho EmployeeListComponent:

<mat-chip-list>
  <md-chip>
    {{ item.num | calculate }}
  </md-chip>
</mat-chip-list>

Vậy là xong! Giờ đây, khi người dùng nhập dữ liệu văn bản liên kết với bất kỳ bộ phận nào, ứng dụng sẽ không tính toán lại giá trị số cho từng nhân viên.

Trong ứng dụng bên dưới, bạn có thể thấy việc nhập liệu mượt mà hơn như thế nào!

Để xem hiệu quả của lần tối ưu hoá gần đây nhất, hãy thử ví dụ này trên StackBlitz.

Bạn có thể xem mã có tính năng tối ưu hoá quy trình thuần tuý của ứng dụng gốc tại đây.

Kết luận

Khi gặp phải tình trạng chậm trong thời gian chạy trong ứng dụng Angular:

  1. Phân tích tài nguyên ứng dụng bằng Công cụ của Chrome cho nhà phát triển để xem nguyên nhân gây ra tình trạng chậm.
  2. Giới thiệu chiến lược phát hiện thay đổi OnPush để cắt bớt các cây con của một thành phần.
  3. Di chuyển các phép tính nặng sang các ống thuần tuý để cho phép khung lưu các giá trị đã tính vào bộ nhớ đệm.