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 theo định kỳ để các thay đổi đối với mô hình dữ liệu được phản ánh trong khung hiển thị của ứng dụng. Tính năng phát hiện thay đổi có thể được kích hoạt 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 lượt 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 rất 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 chính của trình duyệt.

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.

Tính năng phát hiện thay đổi của Inside 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ã cho ứng dụng trong kho lưu trữ GitHub.

Ứng dụng này liệt kê các nhân viên ở hai bộ phận trong công ty là bộ phận bán hàng 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 phiên bản của EmployeeListComponent, một phiên bản cho doanh số và một phiên bản 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 sẽ có một tên và một giá trị số. Ứng dụng chuyển giá trị dạng 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.

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 một nhân viên, thành phần này sẽ kích hoạt kết quả đầu ra 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 phép tính 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 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 trong danh sách. Lớp này cũng bao gồm cả 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 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. Thành phần con là 2 thực thể của EmployeeListComponent. Mỗi phiên bản 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 phòng ban.

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 người dùng không thay đổi kể từ lần kiểm tra gần đây nhất.

Để xem quá trình này chậm đến mức nào, hãy mở phiên bản dự án chưa được tối ưu hoá trên StackBlitz rồi 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 bắt nguồ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 Chrome Công cụ 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.

(ở góc trên cùng bên trái của bảng điều khiển Hiệu suất) và bắt đầu nhập vào một trong các hộp văn bản trong ứng dụng. để 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 có nhiều nhân viên trong danh sách này, 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à gây hiện tượng rớt khung hình, dẫn đến trải nghiệm không tốt cho người dùng.

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

Khi người dùng nhập dữ liệu 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 sẽ 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 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 {...}

Giờ đây, khi người dùng nhập dữ liệu nhập bằng 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 chế độ 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 hoạt động tối ưu hóa này, hãy nhập một nhân viên mới vào ứng dụng trên StackBlitz.

Sử dụng ống tinh khiết

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 bộ phận khi người dùng nhập văn bản vào mục nhập 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 ống nguyên chất và đường ố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ể dùng được trong một mẫu. Sự khác biệt giữa hai loại này là đường ống 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 giá trị cần hiển thị dựa trên giá trị số của nhân viên, bằng cách 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 dấu sổ thẳng, Angular sẽ chỉ tính toán lại biểu thức dấu sổ thẳng khi đối số của nó thay đổi. Khung này sẽ xác định xem các đối số của dấu sổ thẳng có thay đổi hay không bằng cách 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 toán lại nào trừ khi giá trị số của một nhân viên được cập nhật.

Dưới đây là cách chuyển kết quả tính toán kinh doanh vào hệ thống dấu gạch đứ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 dấu sổ thẳng gọi hàm fibonacci. Lưu ý rằng đường ống là thuần tuý. Angular sẽ coi tất cả các đường ống đều 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 liên quan đến bất kỳ phòng ban 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 dưới đây, bạn có thể thấy việc nhập văn bản mượt mà hơn bao nhiêu!

Để 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ể tìm thấy đoạn mã tối ưu hoá đường ống thuần tuý của ứng dụng ban đầu tại đây.

Kết luận

Khi gặp tình trạng làm chậm thời gian chạy trong một ứ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 điểm chậm là từ đâ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. Di chuyển các phép tính phức tạp sang các đường ống thuần tuý để cho phép khung thực hiện thao tác lưu các giá trị đã tính toán vào bộ nhớ đệm.