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

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

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

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

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

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

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

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

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

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

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

คุณจะเห็น EmployeeListComponent 2 รายการในเทมเพลตของ 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 คือคอมโพเนนต์รูทของแอปพลิเคชัน องค์ประกอบย่อยคืออินสแตนซ์ EmployeeListComponent 2 รายการ แต่ละอินสแตนซ์มีรายการ (E1, E2 ฯลฯ) ที่แสดงถึงพนักงานแต่ละคนในแผนก

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

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

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

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

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

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

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

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

เมื่อผู้ใช้พิมพ์อินพุตข้อความสำหรับ sales 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 {...}

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

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

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

อ่านข้อมูลเพิ่มเติมเกี่ยวกับ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 จะถือว่าไพป์ทั้งหมดเป็นแบบ Pure เว้นแต่คุณจะระบุเป็นอย่างอื่น

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

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

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

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

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

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

บทสรุป

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

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