אופטימיזציה של זיהוי השינויים של Angular

הטמעת זיהוי שינויים מהיר יותר לשיפור חוויית המשתמש.

Angular מפעילה מדי פעם מנגנון זיהוי שינויים כדי ששינויים במודל הנתונים יבואו לידי ביטוי בתצוגה של האפליקציה. ניתן להפעיל זיהוי שינויים באופן ידני או באמצעות אירוע אסינכרוני (לדוגמה, אינטראקציה של משתמש או השלמת XHR).

זיהוי השינויים הוא כלי אפקטיבי, אבל אם הוא פועל לעיתים קרובות, הוא יכול להפעיל חישובים רבים ולחסום את ה-thread הראשי של הדפדפן.

בפוסט הזה תלמדו איך לשלוט במנגנון זיהוי השינויים ולבצע אופטימיזציה שלו על ידי דילוג על חלקים באפליקציה והפעלת זיהוי שינויים רק במקרה הצורך.

בתוך זיהוי השינויים של Angular

בואו נסתכל על אפליקציה לדוגמה כדי להבין איך תכונת זיהוי השינויים של Angular פועלת.

תוכלו למצוא את הקוד של האפליקציה במאגר הזה ב-GitHub.

באפליקציה מפורטים עובדים משתי מחלקות בחברה - מכירות ומחקר ופיתוח - והיא כוללת שני רכיבים:

  • AppComponent, שהוא רכיב הבסיס של האפליקציה, וגם
  • שני מופעים של EmployeeListComponent, אחד למכירות והשני למחקר ופיתוח.

אפליקציה לדוגמה

אפשר לראות את שני המופעים של 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 לקישור נתונים דו-כיווני בין הקלט לבין המאפיין label שהוצהר ב-EmployeeListComponent.

עם שני המופעים של EmployeeListComponent, האפליקציה יוצרת את עץ הרכיבים הבא:

עץ רכיבים

AppComponent הוא רכיב הבסיס של האפליקציה. רכיבי הצאצא שלו הם שני המופעים של EmployeeListComponent. לכל מכונה יש רשימה של פריטים (E1, E2 וכו') שמייצגים את העובדים הספציפיים במחלקה.

כשהמשתמש מתחיל להזין שם של עובד חדש בתיבת הקלט ב-EmployeeListComponent, Angular מפעיל זיהוי שינויים עבור כל עץ הרכיבים החל מ-AppComponent. משמעות הדבר היא שבזמן שהמשתמש מקליד את קלט הטקסט, Angular מחשבת מחדש שוב ושוב את הערכים המספריים המשויכים לכל עובד כדי לוודא שהם לא השתנו מאז הבדיקה האחרונה.

כדי לראות כמה איטי זה יכול להיות, פותחים את הגרסה שלא עברה אופטימיזציה של הפרויקט ב-StackBlitz ומנסים להזין שם של עובד.

כדי לוודא שההאטה מגיעה מהפונקציה fibonacci, מגדירים את הפרויקט לדוגמה ופותחים את הכרטיסייה ביצועים בכלי הפיתוח ל-Chrome.

  1. לוחצים על 'Control+Shift+J' (או 'Command+Option+J' ב-Mac) כדי לפתוח את כלי הפיתוח.
  2. לוחצים על הכרטיסייה ביצועים.

כדי להפסיק את ההקלטה. אחרי שכלי הפיתוח ל-Chrome יעבדו את כל נתוני הפרופיילינג שנאספו, תראו משהו כזה:

פרופיילינג של ביצועים

אם יש ברשימה עובדים רבים, התהליך הזה עלול לחסום את ה-thread של ממשק המשתמש של הדפדפן ולגרום לשיבושים במסגרות, דבר שמוביל לחוויית משתמש גרועה.

דילוג על עצי משנה של רכיבים

כשהמשתמש מקליד את קלט הטקסט של המכירות EmployeeListComponent, ברור שהנתונים במחלקת מחקר ופיתוח לא משתנים, ולכן אין סיבה להפעיל זיהוי שינויים ברכיב שלו. כדי שמופע המחקר והפיתוח לא יפעיל זיהוי של שינויים, צריך להגדיר את הערך 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 עדיין מתבצע חישוב מחדש של הערך המספרי לכל העובדים במחלקה כשהמשתמש מקליד את קלט הטקסט המתאים.

כדי לשפר התנהגות זו, אתם יכולים לנצל את היתרונות של צינורות מועילים. גם צינורות טהורים וגם צינורות לא טהורים מקבלים קלט ומחזירים תוצאות שניתן להשתמש בהן בתבנית. ההבדל בין השניים הוא שצינור טהור יחשב מחדש את התוצאה שלו רק אם הוא יקבל קלט שונה מההפעלה הקודמת שלו.

חשוב לזכור שהאפליקציה מחשבת ערך שיוצג על סמך הערך המספרי של העובד, וכך השיטה 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. העברת חישובים כבדים לצינורות עיבוד נתונים טהורים כדי לאפשר ל-framework לבצע שמירה במטמון של הערכים המחושבים.