הטמעת זיהוי שינויים מהיר יותר לשיפור חוויית המשתמש.
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
הוא הרכיב ברמה הבסיסית (root) של האפליקציה. רכיבי הצאצא שלו הם שתי המופעים של EmployeeListComponent
. לכל מופע יש רשימה של פריטים (E1, E2 וכו') שמייצגים את העובדים הספציפיים במחלקה.
כשהמשתמש מתחיל להזין את שם העובד החדש בתיבת הקלט ב-EmployeeListComponent
, Angular מפעילה זיהוי שינויים לכל עץ הרכיבים, החל מ-AppComponent
. המשמעות היא שכשהמשתמש מקלידים את הקלט של הטקסט, Angular מחשבת מחדש שוב ושוב את הערכים המספריים שמשויכים לכל עובד כדי לוודא שהם לא השתנו מאז הבדיקה האחרונה.
כדי לראות כמה התהליך הזה יכול להיות איטי, פותחים את הגרסה ללא אופטימיזציה של הפרויקט ב-StackBlitz ומנסים להזין שם של עובד.
כדי לוודא שההאטה נובעת מהפונקציה fibonacci
, מגדירים את הפרויקט לדוגמה ופותחים את הכרטיסייה ביצועים בכלי הפיתוח ל-Chrome.
- מקישים על Control+Shift+J (או על Command+Option+J ב-Mac) כדי לפתוח את DevTools.
- לוחצים על הכרטיסייה ביצועים.
עכשיו לוחצים על הקלטה (בפינה הימנית העליונה של החלונית ביצועים) ומתחילים להקליד באחד מתיבת הטקסט באפליקציה. אחרי כמה שניות, לוחצים שוב על הקלטה כדי להפסיק את ההקלטה. אחרי שכלי הפיתוח ל-Chrome יעבד את כל נתוני הפרופיל שנאספו, יוצג משהו כזה:
אם יש הרבה עובדים ברשימה, התהליך הזה עלול לחסום את שרשור ממשק המשתמש של הדפדפן ולגרום לירידה במסגרות, מה שמוביל לחוויית משתמש גרועה.
דילוג על עצי משנה של רכיבים
כשהמשתמש מקלידים את הקלט של הטקסט עבור EmployeeListComponent
של sales, אתם יודעים שהנתונים במחלקה 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 עדיין מחשבת מחדש את הערך המספרי של כל העובדים במחלקה כשהמשתמש מקלידים את הקלט הטקסטואלי התואם.
כדי לשפר את ההתנהגות הזו, אפשר להשתמש בצינורות טהורים. צינורות טהורים ומלוכלכים מקבלים קלט ומחזירים תוצאות שאפשר להשתמש בהן בתבנית. ההבדל בין השניים הוא שצינור טהור יחשב מחדש את התוצאה שלו רק אם הוא יקבל קלט שונה מהפעלה הקודמת שלו.
חשוב לזכור שהאפליקציה מחשבת ערך להצגה על סמך הערך המספרי של העובד, באמצעות הפעלת השיטה 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:
- בודקים את פרופיל האפליקציה באמצעות כלי הפיתוח ל-Chrome כדי לראות מה גורם להאטה.
- הצגת שיטת זיהוי השינויים
OnPush
לצורך גיזום של עצי משנה של רכיב. - העברת חישובים כבדים לצינורות טהורים כדי לאפשר למסגרת לבצע אחסון במטמון של הערכים המחושבים.