【问题标题】:Angular performance: ngStyle recalculates on each click on random input角度性能:ngStyle 在每次点击随机输入时重新计算
【发布时间】:2019-07-16 15:00:10
【问题描述】:

我有一个非常愚蠢的性能问题。

我有一个使用ngStyle 的组件,我不想重写它。但是每次我在同一页面上单击随机input(甚至来自另一个组件),ngStyle 都会重新计算(而且速度很慢)。

假设我想要一张带有动态背景的乘法表:

<section>
  <div class="row" 
       *ngFor="let row of rows">
    <div class="col" 
         [ngStyle]="{'background-color': getBG(row*col)}" 
         *ngFor="let col of cols ">
      {{row * col}}
    </div>
  </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>

现在每次我点击这些输入之一 - getBG() 都会被调用。即使该函数只返回一个字符串而不进行任何计算 - 它仍然非常慢

Example at StackBlitz - 只需打开控制台并尝试在不同的输入字段之间快速单击,或输入一个值。即使作为用户,我也可以看到它根本没有响应


UPD1:我的真实案例要复杂得多。并且已经使用ChangeDetectionStrategy.OnPush。将ngStyle 绑定到一个值而不是函数也无济于事——它更快但仍然很慢(并且会产生很多复杂性)。我想要什么,这可能是一种告诉ngStyle 在我明确询问之前不要重新计算的方法。也许ChangeDetectorRef.detach()可以帮忙

【问题讨论】:

  • 也许你最好避免使用该函数并将 bg 颜色映射到矩阵数组(或带有键 [row +'-'+col] 的对象)
  • 这可能对你有帮助:Angular : escape from change detection
  • 模板中的函数调用是通用的性能杀手。不要这样做,在您的组件中运行该函数一次并将其分配为列上的属性并以这种方式分配。

标签: javascript angular typescript


【解决方案1】:

这完全有道理。这就是 Angular 执行变更检测的方式。这是 Angular 执行的额外检查,因为您在其中一种数据绑定语法中调用了函数,这里:

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

Angular 在三种情况下执行变更检测:

  1. DOM 事件。
  2. AJAX 调用。
  3. 超时/间隔。

这是 DOM 事件 (click) 的情况。

现在在执行更改检测时,Angular 会检查组件中的特定变量是否已更改。

就属性而言,这非常简单。但在函数的情况下不是那么直接。

你看,判断一个函数的值是否改变的唯一方法就是调用它。

所以 Angular 就是这样做的。

解决方案:

只需在组件类中为要显示的数字和要绘制的颜色创建一个矩阵:

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%)';
  }

}

然后在你的模板中使用它:

<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>

性能差异:

在之前的实现中,getBG 在初始化时被调用 401 次。

在解决方案实现中,getBG 在初始化时被调用 101 次。

这是大约 397% 的巨大性能提升。

此外,当用户从任何输入字段聚焦和模糊时,不会额外调用 getBG 方法。

这里有一个Working Sample StackBlitz 供您参考。

您可能还想通读我写的关于 Performant Reactive Forms in Angular 的 Medium 文章。虽然它与 Reactive Forms 相关,但我在文章中已经谈到了这方面。我相信你会发现它很有帮助。

【讨论】:

  • 这很有趣并解释了原因,但没有给出如何预防的解决方案。
  • @JeremyThille,啊,我会更新答案。对于那个很抱歉。 :)
  • Angular 通常为异步事件和 DOM 事件执行变更检测,promise 也会与异步管道和其他一些事件一起运行变更检测
  • 老实说,这根本不是一个解决方案。这是一个演示示例,它可以工作,但在现实世界的应用程序中,如果我使用 ngStyle,即使使用单个静态值,我的性能仍然会大幅下降。我更愿意说 Angular “老兄,在我明确要求之前不要重新计算”
  • @SergeiPanfilov,不幸的是,对于这个问题没有 one-size-fits-all 解决方案。对于不同类型的问题和用例,可能有不同的解决方案。我试图根据手头给出的内容来回答。在这方面已经提出了一系列类似的问题。 One was asked today as well。但就像我说的那样,解决方案将在很大程度上取决于特定的用例。我仍然建议您寻找一种一刀切 的解决方案。如果您确实遇到了一个,请将其添加为该线程的答案。祝你好运:)
【解决方案2】:

检测速度慢有两个原因。开发工具有点慢,打印许多消息可以显示更多内容。

另一点是,您正在做不必要的工作。通过分离这两个部分,您将能够将changeDetection 策略更改为OnPush


简化示例:

@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;
  };
}

@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>

OnPush 检测策略意味着,如果给定 Component/Directive 的任何@Inputs 发生变化,它将触发 do 检测。如果你想利用它,你必须将昂贵的部分分成一个单独的指令,同时确保它的@Inputs 只会在必要时改变。


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

【讨论】:

  • 这听起来很有希望,我对这个答案很感兴趣,但 IMO 你解释得太少了。 ChangeDetectionStrategy.OnPush是什么,具体是怎么解决问题的,check if 'generatedTable' needs to be recalculated具体是怎么解决的?
  • OnPush 在这里根本没有帮助 - 因为OnPush 意味着在用户与输入交互后检测更改。检查更新的示例 - 每次点击仍会产生重新计算。
  • @JeremyThille 已更新,立即查看
猜你喜欢
  • 1970-01-01
  • 2014-11-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-02-10
  • 1970-01-01
  • 2017-07-08
相关资源
最近更新 更多