เพิ่มประสิทธิภาพการตรวจหาการเปลี่ยนแปลงของ Angular

ใช้การตรวจหาการเปลี่ยนแปลงที่เร็วขึ้นเพื่อประสบการณ์ของผู้ใช้ที่ดียิ่งขึ้น

Angular จะเรียกใช้กลไกการตรวจหาการเปลี่ยนแปลงเป็นระยะๆ เพื่อให้การเปลี่ยนแปลงในโมเดลข้อมูลแสดงในมุมมองของแอป การตรวจหาการเปลี่ยนแปลงสามารถทริกเกอร์ได้ด้วยตนเองหรือผ่านเหตุการณ์แบบอะซิงโครนัส (เช่น การโต้ตอบของผู้ใช้หรือการดำเนินการ XHR เสร็จสมบูรณ์)

การตรวจหาการเปลี่ยนแปลงเป็นเครื่องมือที่มีประสิทธิภาพ แต่หากเรียกใช้บ่อยเกินไป อาจทำให้เกิดการคำนวณจำนวนมากและบล็อกเธรดของเบราว์เซอร์หลัก

ในโพสต์นี้ คุณจะได้เรียนรู้วิธีควบคุมและเพิ่มประสิทธิภาพกลไกการตรวจหาการเปลี่ยนแปลงโดยการข้ามบางส่วนของแอปพลิเคชันและเรียกใช้การตรวจหาการเปลี่ยนแปลงเมื่อจำเป็นเท่านั้น

การตรวจหาการเปลี่ยนแปลงภายใน Angular

มาดูแอปตัวอย่างเพื่อทำความเข้าใจวิธีการทำงานของการตรวจหาการเปลี่ยนแปลงของ Angular กัน

คุณดูโค้ดของแอปได้ในที่เก็บ GitHub นี้

แอปแสดงรายชื่อพนักงานจาก 2 แผนกในบริษัท ได้แก่ ฝ่ายขายและฝ่ายวิจัยและพัฒนา โดยมีองค์ประกอบ 2 อย่างดังนี้

  • AppComponent ซึ่งเป็นคอมโพเนนต์รูทของแอป และ
  • EmployeeListComponent 2 รายการ รายการหนึ่งสําหรับยอดขาย และอีกรายการสําหรับ R&D

แอปพลิเคชันตัวอย่าง

คุณจะเห็นอินสแตนซ์ 2 รายการของ EmployeeListComponent ในเทมเพลตสำหรับ 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>

พนักงานแต่ละคนจะมีชื่อและค่าที่เป็นตัวเลข แอปจะส่งค่าตัวเลขของพนักงานไปยังการคำนวณทางธุรกิจและแสดงผลลัพธ์บนหน้าจอ

ตอนนี้มาดู 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 รับรายการพนักงานและชื่อแผนกเป็นข้อมูลนำเข้า เมื่อผู้ใช้พยายามนำพนักงานออกหรือเพิ่มพนักงาน คอมโพเนนต์จะทริกเกอร์เอาต์พุตที่เกี่ยวข้อง คอมโพเนนต์ยังกำหนดเมธอด calculate ซึ่งใช้การคำนวณทางธุรกิจด้วย

เทมเพลตสำหรับ 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>

โค้ดนี้จะวนซ้ำพนักงานทั้งหมดในรายการ และแสดงรายการสำหรับพนักงานแต่ละคน นอกจากนี้ ยังมีคำสั่ง ngModel สำหรับการเชื่อมโยงข้อมูลแบบ 2 ทางระหว่างอินพุตกับพร็อพเพอร์ตี้ label ที่ประกาศใน EmployeeListComponent

เมื่อมีอินสแตนซ์ EmployeeListComponent 2 รายการ แอปจะสร้างแผนผังคอมโพเนนต์ต่อไปนี้

แผนผังคอมโพเนนต์

AppComponent เป็นคอมโพเนนต์รูทของแอปพลิเคชัน ส่วนประกอบย่อยคืออินสแตนซ์ 2 รายการของ EmployeeListComponent แต่ละอินสแตนซ์จะมีรายการไอเทม (E1, E2 ฯลฯ) ที่แสดงถึงพนักงานแต่ละคนในแผนก

เมื่อผู้ใช้เริ่มป้อนชื่อพนักงานใหม่ในช่องป้อนข้อมูลในEmployeeListComponent Angular จะทริกเกอร์การตรวจหาการเปลี่ยนแปลงสำหรับทั้งแผนผังคอมโพเนนต์โดยเริ่มจาก AppComponent ซึ่งหมายความว่าขณะที่ผู้ใช้พิมพ์ในช่องป้อนข้อความ Angular จะคำนวณค่าตัวเลขที่เชื่อมโยงกับพนักงานแต่ละคนซ้ำๆ เพื่อยืนยันว่าค่าดังกล่าวไม่ได้เปลี่ยนแปลงตั้งแต่การตรวจสอบครั้งล่าสุด

หากต้องการดูว่าการทำงานนี้ช้าเพียงใด ให้เปิดโปรเจ็กต์เวอร์ชันที่ไม่ได้เพิ่มประสิทธิภาพใน StackBlitz แล้วลองป้อนชื่อพนักงาน

คุณสามารถยืนยันได้ว่าความช้าเกิดจากฟังก์ชัน fibonacci โดยตั้งค่าโปรเจ็กต์ตัวอย่างและเปิดแท็บประสิทธิภาพของเครื่องมือสำหรับนักพัฒนาเว็บใน Chrome

  1. กด `Control+Shift+J` (หรือ `Command+Option+J` ใน Mac) เพื่อเปิด DevTools
  2. คลิกแท็บประสิทธิภาพ

ตอนนี้คลิกบันทึก (ที่มุมซ้ายบนของแผงประสิทธิภาพ) แล้วเริ่มพิมพ์ในช่องข้อความช่องใดช่องหนึ่งในแอป จากนั้นในอีกไม่กี่วินาที ให้คลิกบันทึก อีกครั้งเพื่อหยุดบันทึก เมื่อเครื่องมือสำหรับนักพัฒนาเว็บใน Chrome ประมวลผลข้อมูลการจัดทำโปรไฟล์ทั้งหมดที่รวบรวมแล้ว คุณจะเห็นผลลัพธ์คล้ายกับตัวอย่างต่อไปนี้

การสร้างโปรไฟล์ประสิทธิภาพ

หากมีพนักงานจำนวนมากในรายการ กระบวนการนี้อาจบล็อกเทรด UI ของเบราว์เซอร์และทำให้เฟรมหลุด ซึ่งส่งผลให้ผู้ใช้ได้รับประสบการณ์ที่ไม่ดี

ข้ามซับทรีของคอมโพเนนต์

เมื่อผู้ใช้พิมพ์ข้อความในช่องป้อนข้อความสำหรับยอดขาย EmployeeListComponent คุณจะทราบว่าข้อมูลในแผนกR&D ไม่มีการเปลี่ยนแปลง ดังนั้นจึงไม่มีเหตุผลที่จะต้องเรียกใช้การตรวจหาการเปลี่ยนแปลงในคอมโพเนนต์ของข้อมูล หากต้องการให้แน่ใจว่าอินสแตนซ์ R&D ไม่ทริกเกอร์การตรวจหาการเปลี่ยนแปลง ให้ตั้งค่า changeDetectionStrategy ของ EmployeeListComponent เป็น OnPush ดังนี้

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

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

ตอนนี้เมื่อผู้ใช้พิมพ์ข้อความในช่องป้อนข้อความ ระบบจะทริกเกอร์การตรวจหาการเปลี่ยนแปลงสำหรับแผนกที่เกี่ยวข้องเท่านั้น

การตรวจหาการเปลี่ยนแปลงใน Subtree ของคอมโพเนนต์

คุณจะเห็นการเพิ่มประสิทธิภาพนี้ในแอปพลิเคชันเดิมได้ที่นี่

ดูข้อมูลเพิ่มเติมเกี่ยวกับOnPushกลยุทธ์การตรวจหาการเปลี่ยนแปลงได้ในเอกสารประกอบ Angular อย่างเป็นทางการ

หากต้องการดูผลลัพธ์ของการเพิ่มประสิทธิภาพนี้ ให้ป้อนพนักงานใหม่ในแอปพลิเคชันใน StackBlitz

การใช้ Pure Pipe

แม้ว่าตอนนี้เราจะตั้งค่ากลยุทธ์การตรวจหาการเปลี่ยนแปลงสำหรับ EmployeeListComponent เป็น OnPush แล้ว แต่ Angular ก็ยังคงคำนวณค่าตัวเลขสำหรับพนักงานทั้งหมดในแผนกใหม่เมื่อผู้ใช้พิมพ์ข้อความที่เกี่ยวข้อง

หากต้องการปรับปรุงลักษณะการทำงานนี้ คุณสามารถใช้ประโยชน์จาก Pure Pipe ได้ ทั้ง Pure Pipe และ Impure Pipe จะรับอินพุตและแสดงผลลัพธ์ที่ใช้ในเทมเพลตได้ ความแตกต่างระหว่าง 2 อย่างนี้คือไปป์แบบเพียวจะคำนวณผลลัพธ์ใหม่ก็ต่อเมื่อได้รับอินพุตที่แตกต่างจากการเรียกใช้ครั้งก่อน

โปรดทราบว่าแอปจะคำนวณค่าที่จะแสดงตามค่าตัวเลขของพนักงาน โดยเรียกใช้เมธอด calculate ที่กำหนดไว้ใน EmployeeListComponent หากย้ายการคำนวณไปยัง Pipe ที่ไม่มีการเปลี่ยนแปลง Angular จะคำนวณนิพจน์ Pipe ใหม่เมื่ออาร์กิวเมนต์มีการเปลี่ยนแปลงเท่านั้น เฟรมเวิร์กจะพิจารณาว่าอาร์กิวเมนต์ของไปป์มีการเปลี่ยนแปลงหรือไม่โดยทำการตรวจสอบอ้างอิง ซึ่งหมายความว่า Angular จะไม่ทำการคำนวณใหม่ใดๆ เว้นแต่จะมีการอัปเดตค่าตัวเลขของพนักงาน

วิธีย้ายการคำนวณทางธุรกิจไปยังไปป์ที่ชื่อ 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);
  }
}

เมธอด transform ของไปป์จะเรียกใช้ฟังก์ชัน fibonacci โปรดสังเกตว่าไปป์เป็นฟังก์ชันที่ไม่มีผลข้างเคียง Angular จะถือว่าไปป์ทั้งหมดเป็นแบบ Pure เว้นแต่คุณจะระบุไว้เป็นอย่างอื่น

สุดท้าย ให้อัปเดตนิพจน์ภายในเทมเพลตสำหรับ EmployeeListComponent ดังนี้

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

เท่านี้ก็เรียบร้อย ตอนนี้เมื่อผู้ใช้พิมพ์ข้อความในช่องป้อนข้อความที่เชื่อมโยงกับแผนกใดก็ตาม แอปจะไม่คำนวณค่าตัวเลขสำหรับพนักงานแต่ละคนใหม่

คุณจะเห็นว่าการพิมพ์ลื่นไหลขึ้นมากเพียงใดในแอปด้านล่าง

หากต้องการดูผลลัพธ์ของการเพิ่มประสิทธิภาพครั้งล่าสุด ลองใช้ตัวอย่างนี้ใน StackBlitz

โค้ดที่มีการเพิ่มประสิทธิภาพแบบ Pipe ล้วนของแอปพลิเคชันเดิมพร้อมให้ใช้งานที่นี่

บทสรุป

เมื่อพบว่าแอป Angular ทำงานช้าลงในรันไทม์ ให้ทำดังนี้

  1. สร้างโปรไฟล์แอปพลิเคชันด้วยเครื่องมือสำหรับนักพัฒนาเว็บใน Chrome เพื่อดูว่าความช้ามาจากส่วนใด
  2. แนะนํากลยุทธ์การตรวจหาการเปลี่ยนแปลง OnPush เพื่อตัดแต่งซับทรีของคอมโพเนนต์
  3. ย้ายการคำนวณที่ซับซ้อนไปยัง Pipe ล้วนเพื่อให้เฟรมเวิร์กแคชค่าที่คำนวณได้