Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Performance issues with app-sidebar-nav components due to ngClass bindings #74

Closed
coyoteecd opened this issue Jul 30, 2019 · 6 comments
Closed

Comments

@coyoteecd
Copy link
Contributor

coyoteecd commented Jul 30, 2019

OS: Windows 10
Browsers: Chrome 75.0.3770.142 64-bit, Firefox 68.0.1
Angular 7.2.12

After upgrading to CoreUI 2.4.5 from an old v1.x version, I am experiencing significant performance issues across our entire app. The observed behavior is that typing fast in a text input is laggy; keeping a key pressed shows noticeable hiccups every 5 to 10 characters. All in all, it just gives the feeling of bad UX.

When profiling with Chrome, I noticed that a good deal of time is spent in a "Recalculate Style" step:
image

Switching to Event Log tab in Chrome Debugger while running on a debug version of our app, I noticed that every keypress event triggers a style recalculation. One can find the source of style changes by clicking Reveal link:
image.

Source is AppSidebarNavLinkComponent; our app has 20+ sidebar link items and on every keypress, all of them schedule a style recalculation by removing and re-adding the same CSS classes:
image

Looking at the code, I believe the root cause is the refactoring done in this commit, as well as this older commit. Before those changes, the ngClass was bound to an inline expression, and it was converted to function calls e.g. getLinkClass or helper.getBadgeClass(item). All of these functions always return a new object reference when called, which makes Angular think - during every change detection cycle - that the ngClass attribute has changed. It then proceeds to remove all classes and re-add them back, even if the object structure is the same; verified this by debugging.

A possible solution is to maintain string arrays in each nav item component, and have the getLinkClass/getBadgeClass etc. functions return the same array instance every time is called. NgClass directive has an IterableDiffer to detect changes in the array elements, so clearing the array and re-adding the same items all over again should be okay as long as we keep the same array reference. I verified this and it does get rid of the Recalculate Style, solving the issue.

For example, in app-sidebar-nav-link.component.ts:

  public getLinkClass() {
    const disabled = this.isDisabled();
    const classes = {
      'nav-link': true,
      'disabled': disabled,
      'btn-link': disabled
    };
    if (this.hasVariant()) {
      const variant = `nav-link-${this.item.variant}`;
      classes[variant] = true;
    }
    return classes;
  }

becomes:

  private linkClasses: string[] = [];

  public getLinkClass() {
    this.linkClasses.splice(0);

    this.linkClasses.push('nav-link');

    if (this.isDisabled()) {
      this.linkClasses.push('disabled');
      this.linkClasses.push('btn-link');
    }
    if (this.hasVariant()) {
      const variant = `nav-link-${this.item.variant}`;
      this.linkClasses.push(variant);
    }
    return this.linkClasses;
  }

You would also have to add array variables for badge/icon classes as well, and change SidebarNavHelper to return an array of class names, for the HTML to bind to:

  public getIconClass() {
    this.iconClasses.splice(0);
    for (const iconClass of this.helper.getIconClass(this.item)) {
      this.iconClasses.push(iconClass);
    }

    return this.iconClasses;
  }

It kinda breaks the SidebarNavHelper idea and results in more code; other alternatives might be to introduce an inheritance tree for the app-sidebar-nav-xxx.component classes or to change the SidebarNavHelper to receive the classes array as input.

Let me know what you think. I am willing to fork and submit a pull request if the above changes look acceptable.

@coyoteecd
Copy link
Contributor Author

Related issues in the Angular repo:
angular/angular#25518
angular/angular#28201
angular/angular#27804

@xidedix
Copy link
Member

xidedix commented Aug 1, 2019

Hi @coyoteecd
Thanks for the heads-up and comprehensive analysis of the issue.
We are going to investigate this further.

PR welcome!

@xidedix
Copy link
Member

xidedix commented Aug 2, 2019

We're currently testing the use of custom pipes instead of helper functions in templates for badge/icon classes. Seems promising.

xidedix added a commit that referenced this issue Aug 2, 2019
@xidedix xidedix mentioned this issue Aug 2, 2019
@xidedix
Copy link
Member

xidedix commented Aug 2, 2019

@coyoteecd
Please update to 2.5.3 and let us know if it helps in your case.

@coyoteecd
Copy link
Contributor Author

coyoteecd commented Sep 17, 2019

@xidedix
Sorry for the delayed response, I had to wait for a good time to upgrade our app to Angular 8 to be able to use 2.5.3.

The pipe fix works fine. There is still one left-over however, you'd need to include it here as well:

<i *ngIf="helper.hasIcon(item)" [ngClass]="helper.getIconClass(item)"></i>
<ng-container>{{item.name}}</ng-container>
<span *ngIf="helper.hasBadge(item)" [ngClass]="helper.getBadgeClass(item)">{{ item.badge.text }}</span>
. That component still uses the old helper and consequently triggers the same removeClass/addClass cycle on every key press.

@xidedix
Copy link
Member

xidedix commented Sep 24, 2019

@coyoteecd done - please update to 2.5.5

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants