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 người dùng tốt hơn.

Angular chạy cơ chế phát hiện thay đổi theo đị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ụ: lượt tương tác của người dùng hoặc hoàn thành XHR).

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, công cụ 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 các phần của ứng dụng và chỉ chạy tính năng phát hiện thay đổi khi cần thiết.

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 cùng xem một ứng dụng mẫu!

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

Ứng dụng liệt kê nhân viên ở 2 phòng ban trong công ty là bộ phận kinh doanh và bộ phận R&D, đồng thời 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 dành cho bộ phận bán hàng và một dành cho hoạt động nghiên cứu và phát triển.

Ứng dụng mẫu

Bạn có thể xem hai thực thể của EmployeeListComponent trong mẫu của 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 đều có tên và giá trị số. Ứng dụng chuyển giá trị số của nhân viên sang một phép tính kinh doanh và trực quan hoá kết quả trên màn hình.

Bây giờ, hãy cùng 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 một nhân viên, thành phần này sẽ kích hoạt một kết quả tương ứng. Thành phần này cũng xác định phương thức calculate. Phương thức này sẽ triển khai việc tính toán kinh doanh.

Dưới đâ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à đối với mỗi nhân viên, sẽ hiển thị một mục danh sách. 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 2 thực thể của EmployeeListComponent, ứng dụng sẽ tạo 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 là 2 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 ô nhập dữ liệu 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 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 họ chưa thay đổi kể từ lần kiểm tra cuối cùng.

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

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

  1. Nhấn tổ hợp phím "Control+Shift+J" (hoặc "Command+Option+J" trên máy Mac) để mở Công cụ cho nhà phát triển.
  2. Nhấp vào thẻ Hiệu suất.

để 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 lập hồ sơ đã thu thập, bạn sẽ thấy như sau:

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

Nếu danh sách có nhiều nhân viên, thì quá trình này có thể chặn luồng giao diện người dùng của trình duyệt và làm giảm khung hình, từ đó đem lại trải nghiệm không tốt cho người dùng.

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

Khi người dùng nhập 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 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 của bộ phận này. Để đảm bảo phiên bản 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 {...}

Bây giờ, khi người dùng nhập nội dung 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ể xem tính năng tối ưu hoá này được áp dụng cho ứng dụng ban đầu 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 vào ứng dụng trên StackBlitz.

Dùng tẩu thuốc tinh khiết

Mặc dù chiến lược phát hiện thay đổi cho EmployeeListComponent hiện được thiết lập thành OnPush, nhưng Angular vẫn tính toán lại giá trị số của toàn bộ nhân viên trong bộ phận khi người dùng nhập 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 ống thuần tuý. Cả ống thuần túy và ống không tinh khiết đều chấp nhận dữ liệu đầu vào và trả về kết quả có thể sử dụng được trong mẫu. Điểm khác biệt giữa hai hệ thống này là một đường ống thuần tuý sẽ chỉ tính toán lại kết quả nếu nhận được đầu vào khác với lệnh gọi trước đó.

Hãy nhớ rằng ứng dụng tính toán 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 chuyển phép tính sang một đường ống thuần tuý, Angular sẽ chỉ tính toán lại biểu thức gạch đứng khi đối số của nó thay đổi. Khung này sẽ xác định xem các đối số của đường ống đã thay đổi hay chưa bằng cách kiểm tra tệp đối chiếu. Điều này có nghĩa là Angular sẽ không thực hiện bất kỳ tính toán lại nào trừ khi 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 ống có tên 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 đường ống sẽ gọi hàm fibonacci. Lưu ý rằng đường ống này hoàn toàn tinh khiết. Angular sẽ coi tất cả các đường ống là 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 văn bản nhập liên quan đến bộ phận bất kỳ, ứ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 dưới đây, bạn có thể thấy quá trình nhập mượt mà hơn!

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

Mã có tính năng tối ưu hoá đường ống thuần tuý của ứng dụng ban đầu hiện có tại đây.

Kết luận

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

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