כדאי להטמיע זיהוי שינויים מהיר יותר לשיפור חוויית המשתמש.
חברת Angular מפעילה את מנגנון זיהוי השינויים שלה מדי פעם כדי ששינויים במודל הנתונים ישתקפו בתצוגה של האפליקציה. אפשר להפעיל את זיהוי השינויים באופן ידני או באמצעות אירוע אסינכרוני (לדוגמה, אינטראקציה של משתמש או השלמת XHR).
זיהוי שינויים הוא כלי רב-עוצמה, אבל אם הוא פועל לעיתים קרובות מאוד, הוא עלול להפעיל הרבה חישובים ולחסום את ה-thread הראשי של הדפדפן.
בפוסט הזה תלמדו איך לשלוט במנגנון זיהוי השינויים ולבצע אופטימיזציה שלו על ידי דילוג על חלקים באפליקציה והפעלת זיהוי השינויים רק במקרה הצורך.
זיהוי השינויים של Inside 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
הוא הרכיב ברמה הבסיסית (root) של האפליקציה. רכיבי הצאצא שלו הם שתי המופעים של EmployeeListComponent
. לכל מופע יש רשימה של פריטים (E1, E2 וכו') שמייצגים את העובדים הספציפיים במחלקה.
כשהמשתמש מתחיל להזין את שם העובד החדש בתיבת הקלט ב-EmployeeListComponent
, Angular מפעילה זיהוי שינויים לכל עץ הרכיבים, החל מ-AppComponent
. המשמעות היא שכשהמשתמש מקלידים את הקלט של הטקסט, Angular מחשבת מחדש שוב ושוב את הערכים המספריים שמשויכים לכל עובד כדי לוודא שהם לא השתנו מאז הבדיקה האחרונה.
כדי לבדוק עד כמה זה יכול להיות איטי, אפשר לפתוח את הגרסה הלא מותאמת של הפרויקט ב-StackBlitz ולנסות להזין שם של עובד.
כדי לוודא שההאטה מגיעה מהפונקציה fibonacci
, צריך להגדיר את הפרויקט לדוגמה ולפתוח את הכרטיסייה ביצועים בכלי הפיתוח ל-Chrome.
- מקישים על 'Control+Shift+J' (או על 'Command+Option+J' ב-Mac) כדי לפתוח את כלי הפיתוח.
- לוחצים על הכרטיסייה ביצועים.
עכשיו לוחצים על הקלטה (בפינה הימנית העליונה של החלונית ביצועים) ומתחילים להקליד באחת מתיבות הטקסט באפליקציה. בעוד כמה שניות, לוחצים שוב על הקלטה כדי להפסיק את ההקלטה. לאחר שכלי הפיתוח ל-Chrome יעבדו את כל נתוני הפרופיילינג שנאספו, תראו משהו כזה:
אם יש הרבה עובדים ברשימה, התהליך הזה עלול לחסום את שרשור ממשק המשתמש של הדפדפן ולגרום לירידה במסגרות, מה שמוביל לחוויית משתמש גרועה.
דילוג על עצי משנה של רכיבים
כשהמשתמש מקליד את קלט הטקסט של יחידת המכירות EmployeeListComponent
, אתם יודעים שהנתונים במחלקת המו"מ לא משתנים, כך שאין סיבה להפעיל זיהוי שינויים ברכיב שלהם. כדי לוודא שמכונת המחקר וה-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 עדיין מחשבת מחדש את הערך המספרי של כל העובדים במחלקה כשהמשתמש מקלידים את הקלט הטקסטואלי התואם.
כדי לשפר את ההתנהגות הזו, אפשר להשתמש בקו ניצבות בלבד. צינורות טהורים ומלוכלכים מקבלים קלט ומחזירים תוצאות שאפשר להשתמש בהן בתבנית. ההבדל בין השניים הוא שצינור טהור יחשב מחדש את התוצאה שלו רק אם הוא יקבל קלט שונה מהפעלה הקודמת שלו.
חשוב לזכור שהאפליקציה מחשבת ערך שיוצג על סמך הערך המספרי של העובד, תוך הפעלת השיטה calculate
שמוגדרת ב-EmployeeListComponent
. אם מעבירים את החישוב לקווים אנכיים, מערכת Angular תחשב מחדש את הביטוי של הקו הניצב רק כשהארגומנטים שלה משתנים. ה-framework יקבע אם הארגומנטים של ה-צינור עיבוד הנתונים השתנו על ידי ביצוע בדיקת הפניה. כלומר, 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);
}
}
ה-method transform
של הצינור מפעילה את הפונקציה fibonacci
. שימו לב שהצינור נקי. מערכת Angular תתייחס לכל הצינורות כטהורים, אלא אם יצוין אחרת.
לסיום, מעדכנים את הביטוי בתוך התבנית של EmployeeListComponent
:
<mat-chip-list>
<md-chip>
{{ item.num | calculate }}
</md-chip>
</mat-chip-list>
זהו! עכשיו, כשהמשתמש מקליד את קלט הטקסט שמשויך למחלקה כלשהי, האפליקציה לא תחשב מחדש את הערך המספרי לעובדים ספציפיים.
באפליקציה שלמטה ניתן לראות עד כמה ההקלדה חלקה יותר!
כדי לראות את ההשפעה של האופטימיזציה האחרונה, נסו את הדוגמה הזו ב-StackBlitz.
הקוד עם אופטימיזציית צינור טהורה של האפליקציה המקורית זמין כאן.
סיכום
במקרה של האטה בזמן הריצה באפליקציה Angular:
- יוצרים פרופיל של האפליקציה באמצעות כלי הפיתוח ל-Chrome כדי לראות מאיפה מגיעות האטות.
- מציגים את האסטרטגיה לזיהוי שינויים
OnPush
כדי להסיר את עצי המשנה של רכיב. - העברת חישובים כבדים לצינורות טהורים כדי לאפשר למסגרת לבצע שמירת נתונים במטמון של הערכים המחושבים.