Reputation: 57
I am experiencing issues with performance in one of my components. Here's a link to a video showing these performance-issues: https://vimeo.com/342044142
In the video I am noticing several seconds of delay between when I am selecting to show deleted/read alerts and when they are actually shown. The UI freezes for a few seconds.
I am also experiencing performance-issues when using the search-box to search for alerts. Not so much when I am typing in the search-criteria but rather when I am erasing the current search-criteria so that all the alerts are shown. In the video I am erasing at a constant speed but when I am erasing the 1-2 last letters, I am experiencing a freeze.
I am using a custom pipe for the search-logic as shown in alerts-filter.pipe.ts.
For each set of alerts that are being rendered, I am looping through an Observable containing alertsCreatedToday, alertsCreatedAWeekAgo, alertsCreatedAMonthAgo and alertsCreatedMoreThanAMonthAgo and using the alerts-filter-pipe to render the results that matches the search-criteria.
I have a lot of logic inside my template. I don't know if this is what is causing the issues explained above or if there's something else. I am new to Angular and the way I have implemented this component is the only way I have been able to figure out how to achieve my desired logic. Alerts should be rendered based on certain parameters as can be seen in the method "parametersAreValid()". The heading should also be rendered if there are alerts that fulfill the parameters so that no heading appears without any alerts below it.
I would be very grateful if anyone could point me in the right direction regarding what might be causing these issues, and what I could change to improve the performance of this component.
alerts-page.component.html
<div class="page-content">
<div class="top" fxLayout.xs="column" fxLayout.gt-xs="row">
<div fxLayout="row" fxLayout.xs="column" fxLayoutAlign="start center" fxLayoutAlign.xs="start start" fxFlex="50">
<app-page-header title="Alerts" icon="notification_important" class="mr-4"></app-page-header>
<button *ngIf="!loadingIndicator"
(click)="showCreateDialog(groups, users)">
Create new alert...
</button>
</div>
<div *ngIf="!loadingIndicator">
<mat-form-field class="mr-3">
<input [(ngModel)]="searchTerm" matInput>
</mat-form-field>
</div>
</div>
<mat-selection-list #alertSettings>
<mat-list-option
[selected]="showDeleted" (click)="showDeletedClicked()">Show deleted
</mat-list-option>
<mat-list-option
[selected]="showRead" (click)="showReadClicked()">Show read
</mat-list-option>
</mat-selection-list>
<h2 *ngIf="(!loadingIndicator) && (headingShouldBeRendered(alertsCreatedToday$ | async) > 0)">Today</h2>
<ng-container *ngFor="let alert of alertsCreatedToday$ | async |
alertsFilter:searchTerm; trackBy: trackByFn1">
<alert *ngIf="parametersAreValid(alert.alertRecipient) && !loadingIndicator"
[alertRecipient]="alert.alertRecipient"
[alertMessage]="alert.alertMessage">
</alert>
</ng-container>
<h2 *ngIf="(!loadingIndicator) && (headingShouldBeRendered(alertsCreatedAWeekAgo$ | async) > 0)">Last Week</h2>
<ng-container *ngFor="let alert of alertsCreatedAWeekAgo$ | async |
alertsFilter:searchTerm; trackBy: trackByFn2">
<alert *ngIf="parametersAreValid(alert.alertRecipient) && !loadingIndicator"
[alertRecipient]="alert.alertRecipient"
[alertMessage]="alert.alertMessage">
</alert>
</ng-container>
<h2 *ngIf="(!loadingIndicator) && (headingShouldBeRendered(alertsCreatedAMonthAgo$ | async) > 0)">Last Month</h2>
<ng-container *ngFor="let alert of alertsCreatedAMonthAgo$ | async |
alertsFilter:searchTerm; trackBy: trackByFn3">
<alert *ngIf="parametersAreValid(alert.alertRecipient) && !loadingIndicator"
[alertRecipient]="alert.alertRecipient"
[alertMessage]="alert.alertMessage">
</alert>
</ng-container>
<h2 *ngIf="(!loadingIndicator) && (headingShouldBeRendered(alertsCreatedMoreThanAMonthAgo$ | async) > 0)">Earlier</h2>
<ng-container *ngFor="let alert of alertsCreatedMoreThanAMonthAgo$ | async |
alertsFilter:searchTerm; trackBy: trackByFn4">
<alert *ngIf="parametersAreValid(alert.alertRecipient) && !loadingIndicator"
[alertRecipient]="alert.alertRecipient"
[alertMessage]="alert.alertMessage">
</alert>
</ng-container>
</div>
alerts-page.component.ts
@Component({
selector: 'alerts-page',
templateUrl: './alerts-page.component.html',
styleUrls: ['./alerts-page.component.scss'],
animations: [fadeInOut],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AlertsPageComponent implements OnInit, AfterContentChecked {
@ViewChild('alertSettings') alertSettings: MatSelectionList;
loadingIndicator: boolean;
alertMessages$: Observable<AlertMessage[]>;
alertsCreatedToday$: Observable<Alert[]>;
alertsCreatedAWeekAgo$: Observable<Alert[]>;
alertsCreatedAMonthAgo$: Observable<Alert[]>;
alertsCreatedMoreThanAMonthAgo$: Observable<Alert[]>;
alertMessagesFromServer: AlertMessage[];
alertMessagesFromClient: AlertMessage[];
alertRecipients: AlertRecipient[];
currentUser: User = new User();
groups: Group[] = [];
users: User[] = [];
newMessages: AlertMessage[];
searchTerm: string;
showDeleted = false;
showRead = true;
alertMessages: AlertMessage[];
constructor(private alertMessagesService: AlertMessagesService,
private alertsService: AlertsService,
private notificationMessagesService: NotificationMessagesService,
private dialog: MatDialog,
private usersService: UsersService,
private groupService: GroupsService,
private changeDetectorRef: ChangeDetectorRef) { }
ngOnInit() {
this.loadData();
this.initializeObservables();
}
private initializeObservables() {
this.alertMessages$ = this.alertMessagesService.messages;
this.alertsCreatedToday$ = this.alertMessagesService.alertsCreatedToday;
this.alertsCreatedAWeekAgo$ = this.alertMessagesService.alertsCreatedAWeekAgo;
this.alertsCreatedAMonthAgo$ = this.alertMessagesService.alertsCreatedAMonthAgo;
this.alertsCreatedMoreThanAMonthAgo$ = this.alertMessagesService.alertsCreatedMoreThanAMonthAgo;
}
private loadData() {
this.notificationMessagesService.startLoadingMessage();
this.loadingIndicator = true;
this.currentUser = this.usersService.currentUser;
forkJoin(
this.alertsService.getAlertMessagesForUser(this.currentUser.id),
this.groupService.getGroups(),
this.usersService.getUsers()
).subscribe(
result => this.onDataLoadSuccessful(result[0], result[1], result[2]),
error => this.onDataLoadFailed(error)
);
}
private onDataLoadSuccessful(alertMessagesFromServer: AlertMessage[], groups: Group[], users: User[]) {
this.notificationMessagesService.stopLoadingMessage();
this.loadingIndicator = false;
this.alertMessagesFromServer = alertMessagesFromServer;
this.groups = groups;
this.users = users;
this.alertMessagesService.messages.subscribe(
(alertMessagesFromClient: AlertMessage[]) => this.alertMessagesFromClient = alertMessagesFromClient
);
if (this.newMessagesFromServer()) {
this.newMessages = _.differenceBy(this.alertMessagesFromServer, this.alertMessagesFromClient, 'id');
this.newMessages.map((message: AlertMessage) => this.alertMessagesService.addMessage(message));
}
}
private onDataLoadFailed(error: any): void {
this.notificationMessagesService.stopLoadingMessage();
this.loadingIndicator = false;
this.notificationMessagesService.showStickyMessage('Load Error', `Unable to retrieve alerts from the server.\r\nErrors: "${Utilities.getHttpResponseMessage(error)}"`,
MessageSeverity.error, error);
}
private newMessagesFromServer(): boolean {
if (this.alertMessagesFromClient == null && this.alertMessagesFromServer != null) {
return true;
} else if (this.alertMessagesFromServer.length > this.alertMessagesFromClient.length) {
return true;
} else {
return false;
}
}
getAlertMessageForRecipient(alertRecipient: AlertRecipient): AlertMessage {
for (const alert of this.alertMessagesFromServer) {
if (alert.id === alertRecipient.alertId) {
return alert;
}
}
}
parametersAreValid(alertRecipient: AlertRecipient): boolean {
const isForCurrentUser = alertRecipient.recipientId === this.currentUser.id;
const isNotMarkedAsDeleted = !alertRecipient.isDeleted;
const isNotMarkedAsRead = !alertRecipient.isRead;
const isShownWhenShowDeletedIsSetToTrue = (alertRecipient.isDeleted && this.showDeleted);
const isShownWhenShowReadIsSetToTrue = (alertRecipient.isRead && this.showRead);
return (isForCurrentUser) && (isShownWhenShowDeletedIsSetToTrue || isNotMarkedAsDeleted) &&
(isNotMarkedAsRead || isShownWhenShowReadIsSetToTrue);
}
headingShouldBeRendered(alerts: Alert[]): number {
let count = 0;
if (alerts) {
for (const alert of alerts) {
if (this.parametersAreValid(alert.alertRecipient)) {
count++;
}
}
}
return count;
}
showDeletedClicked() {
this.showDeleted = this.alertSettings.options.first.selected;
}
showReadClicked() {
this.showRead = this.alertSettings.options.last.selected;
}
trackByFn1(item, index) {
return item.alertId;
}
trackByFn2(item, index) {
return item.alertId;
}
trackByFn3(item, index) {
return item.alertId;
}
trackByFn4(item, index) {
return item.alertId;
}
}
alerts-filter.pipe.ts
@Pipe( {
name: 'alertsFilter'
})
export class AlertsFilterPipe implements PipeTransform {
transform(alerts: Alert[], searchTerm: string): Alert[] {
if (!alerts || !searchTerm) {
return alerts;
}
return alerts.filter(alert =>
alert.alertMessage.author.fullName.toLocaleLowerCase().indexOf(searchTerm.toLowerCase()) !== -1);
}
}
alert.component.html
<mat-card>
<mat-card-header>
<div [ngSwitch]="alertRecipient.isRead" (click)="toggleIsRead(alertRecipient)">
<mat-icon *ngSwitchCase="true">drafts</mat-icon>
<mat-icon *ngSwitchCase="false">markunread</mat-icon>
</div>
</mat-card-header>
<mat-card-content>
<div class="avatar-wrapper" fxFlex="25">
<img [src]="getAvatarForAlert(alertMessage)" alt="User Avatar">
</div>
<h3>{{alertMessage.title}}</h3>
<p>{{alertMessage.body}}</p>
</mat-card-content>
<mat-card-actions>
<button>DELETE</button>
<button>DETAILS</button>
</mat-card-actions>
</mat-card>
Upvotes: 2
Views: 731
Reputation: 57
After implementing Freddys solution, there was unfortunately no improvement in regards to performance.
The solution using pipes looks like this:
alert-array-checking.pipe.ts
@Pipe({
name: 'alertArrayChecking'
})
export class AlertArrayCheckingPipe implements PipeTransform {
alertCheckingPipe = new AlertCheckingPipe();
transform(value: any[], options: any): boolean {
return Array.isArray(value) && value.some(arrayElement => this.alertCheckingPipe.transform(arrayElement, options));
}
}
alert-checking.pipe.ts
@Pipe({
name: 'alertChecking'
})
export class AlertCheckingPipe implements PipeTransform {
transform(alert: any, options: any): boolean {
const isForCurrentUser = alert.alertRecipient.recipientId === options.currentUserId;
const isNotMarkedAsDeleted = !alert.alertRecipient.isDeleted;
const isNotMarkedAsRead = !alert.alertRecipient.isRead;
const isShownWhenShowDeletedIsSetToTrue = (alert.alertRecipient.isDeleted && options.showDeleted);
const isShownWhenShowReadIsSetToTrue = (alert.alertRecipient.isRead && options.showRead);
const alertShouldBeRendered = (isForCurrentUser) && (isShownWhenShowDeletedIsSetToTrue || isNotMarkedAsDeleted) &&
(isNotMarkedAsRead || isShownWhenShowReadIsSetToTrue);
return alertShouldBeRendered;
}
}
alerts-page.component.html
<ng-container *ngIf="!loadingIndicator && alertsCreatedToday$ | async as alertsCreatedToday">
<h2 [@fadeInOut] *ngIf="alertsCreatedToday | alertArrayChecking:options">Today</h2>
<div>
<ng-container *ngFor="let alert of alertsCreatedToday">
<alert *ngIf="alert | alertChecking:options"
[alertRecipient]="alert.alertRecipient"
[alertMessage]="alert.alertMessage">
</alert>
</ng-container>
</div>
</ng-container>
Upvotes: 0
Reputation: 872
Can't comment, but I'd recommend replacing the *ngIf's you have that point to functions to point to variables where possible or to pipes. Angular's change detection is not able to cache the result of functions, so they are re-ran every cycle which can add up very quickly.
This is roughly what you'd need to do based on the today section. I'd recommend trying how you have it now with some console logs in your functions and compare it to this. Create two pipes:
@Pipe({ name: 'alertCheckingPipe' })
export class AlertCheckingPipe implements PipeTransform {
transform(alertRecipient: any, options: any): boolean {
// logging to illustrate how little pipe logic gets called
console.log('alertCheckingPipe');
// replace with your more specific parameter validation logic
return alertRecipient.recipientId === options.currentUser.id;
}
}
And one handling an array delegating back to the first
@Pipe({ name: 'alertArrayCheckingPipe' })
export class AlertArrayCheckingPipe implements PipeTransform {
alertCheckingPipe = new AlertCheckingPipe();
transform(value: any[], options: any): boolean {
console.log('alertArrayCheckingPipe');
return Array.isArray(value) && value.some(arrayElement => this.alertCheckingPipe.transform(arrayElement, options));
}
}
Declare those in your module, then update the html to something like:
<div *ngIf="!loadingIndicator && alertsCreatedToday$ | async as results">
<h2 *ngIf="results | alertArrayCheckingPipe:options">
TODAY
</h2>
<ng-container *ngFor="let alert of results">
<div *ngIf="alert | alertCheckingPipe:options">
{{ alert.recipientId }}
</div>
</ng-container>
</div>
There's also a bit of restructuring there, I'm only using the async pipe once and then letting the result be stored in a "results" variable that is then use in the inner elements.
And here's the backing data objects I was using for my testing:
this.alertsCreatedToday$ = of([{ recipientId: 'testId', isDeleted: false, isRead: true }, //
{ recipientId: 'badId', isDeleted: false, isRead: true }]).pipe(delay(2000));
this.options = { currentUser: { id: 'testId' }, showDeleted: true, showRead: true };
Upvotes: 1