Angular performance: ngStyle recalculates on each click on random input

前端 未结 2 1232
醉酒成梦
醉酒成梦 2021-01-06 10:53

I have a very stupid performance issue.

I have a component that uses ngStyle, and I\'d prefer to not rewrite it. But each time I click

相关标签:
2条回答
  • 2021-01-06 11:14

    That makes perfect sense. This is how Angular performs change detection. And this is Angular performing extra checks since you called a function in one of the data-binding syntaxes, here:

    [ngStyle]="{'background-color': getBG(row*col)}" 
    

    Angular performs Change Detection in three cases:

    1. DOM Events.
    2. AJAX Calls.
    3. Timeouts / Intervals.

    This is the case of DOM Events (click).

    Now when performing Change Detection, Angular check whether a particular variable in the Component has changed.

    That's pretty straight forward in case of properties. But not so straight-forward in case of functions.

    You see, the only way to determine whether the value of a function has changed is by calling it.

    So Angular is doing just that.

    SOLUTION:

    Just create a matrix for the number to show and the color to paint right in the Component Class:

    import { Component } from '@angular/core';
    
    @Component({
      selector: 'my-app',
      templateUrl: './app.component.html',
      styleUrls: ['./app.component.css']
    })
    export class AppComponent {
      rows = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
      cols = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
      matrix = [];
    
      model1 = '';
      model2 = '';
      model3 = '';
      model4 = '';
      model5 = '';
    
      ngOnInit() {
        this.rows.forEach((row, rowIndex) => {
          this.matrix.push([]);
          this.cols.forEach((col, colIndex) => {
            const product = row * col;
            this.matrix[row].push({
              numberToShow: product,
              color: this.getBG(product),
            });
          })
        });
      }
    
      getBG(hue: number): string {
        console.log('getBG was called');
        return 'hsl(' + hue + ', 100%, 50%)';
      }
    
    }
    

    And then use it in your template:

    <br/>
    <div> 1. Open a console</div>
    <br/>
    
    <section>
        <div class="row" *ngFor="let row of matrix">
            <div 
          class="col" 
          [style.background-color]="col.color" 
          *ngFor="let col of row ">
                {{col.numberToShow}}
            </div>
        </div>
    </section>
    
    <br/>
    <div>2. Click fast on the different inputs: </div>
    <br/>
    
    <section>
        <input type="text" [ngModel]="model1"/>
      <input type="text"[ngModel]="model2"/>
      <input type="text"[ngModel]="model3"/>
      <input type="text"[ngModel]="model4"/>
      <input type="text"[ngModel]="model5"/>
    </section>
    

    Difference in the performance:

    In the previous implementation, the getBG was called 401 times on initialization.

    In the solution implementation, the getBG is called 101 times on initialization.

    That's a massive performance gain of around 397%.

    Plus there's no extra call to the getBG method when the user focuses and blurs out from any input field.

    Here's a Working Sample StackBlitz for your ref.

    You might also want to read through a Medium Article that I wrote about Performant Reactive Forms in Angular. Although it's related to Reactive Forms, I've touched upon this aspect, in the article. I'm sure you'll find it helpful.

    0 讨论(0)
  • 2021-01-06 11:31

    The detection is slow for 2 reasons. Dev tools are kinda slow and printing many messages can show things even more.

    The other point is, that you are doing unnecessary work. By separating the two parts you will be able to change the changeDetection strategy to OnPush.


    Simplified example:

    @Component({
        selector: 'my-cell',
        template: '<div [ngStyle]="styles"><ng-content></ng-content></div>',
        changeDetection: ChangeDetectionStrategy.OnPush,
    })
    export class CellComponent {
        @Input() styles: {
        readonly "background-color": string;
      };
    }
    
    

    and

    @Component({
        selector: 'my-app',
        templateUrl: './app.component.html',
        styleUrls: ['./app.component.css']
    })
    export class AppComponent {
        rows = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
        cols = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
        matrix: {
            numberToShow: number;
            styles: {
                readonly "background-color": string;
            };
        }[][] = [];
    
        model1 = '';
        model2 = '';
        model3 = '';
        model4 = '';
        model5 = '';
    
        ngOnInit() {
            this.rows.forEach((row, rowIndex) => {
                this.matrix.push([]);
                this.cols.forEach((col, colIndex) => {
                    const product = row * col;
                    const self = this;
                    this.matrix[row].push({
                        numberToShow: product,
                        styles: {
                            get "background-color"() {
                                console.log('background-color read');
                                return self.getBG(product)
                            },
                        },
                    });
                })
            });
        }
    
        getBG(hue: number): string {
            return 'hsl(' + hue + ', 100%, 50%)';
        }
    }
    
    
    <section>
        <div class="row" *ngFor="let row of matrix">
            <my-cell [styles]="col.styles" *ngFor="let col of row">
                {{col.numberToShow}}
            </my-cell>
        </div>
    </section>
    
    <section>
        <input type="text" [ngModel]="model1"/>
        <input type="text" [ngModel]="model2"/>
        <input type="text" [ngModel]="model3"/>
        <input type="text" [ngModel]="model4"/>
        <input type="text" [ngModel]="model5"/>
    </section>
    

    The OnPush detection strategy means that, if any of @Inputs of given Component/Directive changes, it will trigger do detection. If you want to make use of that, you have to separate the expensive part into a separate directive, while making sure, its @Inputs will only change if necessary.


    StackBlitz: https://stackblitz.com/edit/style-performance-of-a-grid-fzbzkz

    0 讨论(0)
提交回复
热议问题