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

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

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

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

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

การตรวจจับการเปลี่ยนแปลงของ Angular

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

คุณดูโค้ดสำหรับแอปได้ในที่เก็บ GitHub นี้

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

  • AppComponent ซึ่งเป็นคอมโพเนนต์รูทของแอป และ
  • EmployeeListComponent 2 อินสแตนซ์ รายการหนึ่งสำหรับการขายและอีกรายการหนึ่งสำหรับการวิจัยและพัฒนา

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

คุณดูอินสแตนซ์ 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 DevTools

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

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

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

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

การข้ามโครงสร้างย่อยของคอมโพเนนต์

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

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

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

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

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

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

การใช้ท่อบริสุทธิ์

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

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

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

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

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

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

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

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

โค้ดที่มีการเพิ่มประสิทธิภาพไปป์ท่อบริสุทธิ์ของแอปพลิเคชันเดิมสามารถดูได้ที่นี่

บทสรุป

เมื่อแอป Angular ทำงานช้าลง

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