优化 Angular 应用的性能小贴士

作者:Netanel Basal 发表日期:2017-09-09 11:52:08 更新日期:2017-09-09 12:01:51 分类:猿文色

摘要

转载: https://netbasal.com/optimizing-the-performance-of-your-angular-application-f222f1c16354

正文

1. Using OnPush

By default, Angular runs change detection on all components every time something changes in your app — from a click event to data received from an ajax call. ( user events, timers, xhr, promises, etc. )

Imagine, for example, that we have a select component.

@Component({
  selector: 'my-select',
  template: `
  <select (change)="change()">
    <option *ngFor="let option of options" 
            [value]="option.id">
      {{option.name}}
    </option>
  </select>
  `
})
export class MySelectComponent {
  @Input() options = [];
  change() {}
}

We are going to pass an array of skills to the select component and will set the properties as “getters” so we can see when Angular checks the values.

class Skill {
  constructor( private _id, private _name ) {}

  get id() {
    console.log('Checking id');
    return this._id;
  }

  get name() {
    console.log('Checking name');
    return this._name;
  }
}

@Component({
  template: `
    <my-select [options]="skills"></my-select>
    <button (click)="trigger()">Trigger change detection</button>
  `
})
export class AppComponent {
  skills = [ new Skill(1, 'JS'), new Skill(2, 'CSS'), new Skill(3, 'Angular') ]
  trigger() {}
}

When you press the button, Angular will run a change detection cycle. Because we are in development mode, it will happen twice for each binding, so, in our case, the calculation is:

option.id * 2
     +
option.name * 2
     *  
  3 options
     =
    12


Although Angular is very fast, as your app grows, Angular will have to work harder to keep track of all the changes.
What if we could help Angular and give her a better indication of when to check our component?
We can set the ChangeDetectionStrategy of our component to ChangeDetectionStrategy.OnPush . This tells Angular that the component only depends on his Inputs ( aka pure ) and needs to be checked in only the following cases:

    1. The Input reference changes.

    2. An event occurred from the component or one of his children.

    3. You run change detection explicitly by calling detectChanges()/tick()/markForCheck() .

@Component({
  selector: 'my-select',
  template: `
    ...
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})

Because none of the conditions mentioned above occurred, Angular will not check the component at the current change detection cycle. Win win.


2. Using TrackBy

At this stage, we have not yet done as much as we can. If, at some point, we need to change the data in the collection ( this.skills), maybe as a result of an API request, we will be faced with a problem, because Angular can’t keep track of items in the collection and has no knowledge of which items have been removed or added.

As a result, Angular needs to remove all the DOM elements associated with the data and create them again. This can mean a lot of DOM manipulations, especially in the case of a big collection. And, as we know, DOM manipulations are expensive.

Let’s see this in action.

export class AppComponent {
  skills = [ ... ]

  ngOnInit() {
    setTimeout(() => {
      this.skills = [ ...same skills as before, new Skill(4, 'Typescript') ]
    }, 4000);
  }
}


If we provide a trackBy function, Angular can track which items have been added or removed according to the unique identifier and only create or destroy the things that have changed.



3. Avoid Computing Values in the Template

Sometimes you need to transform a value that comes from the server to something you can display in the UI.

For example:

@Component({
  selector: 'skills',
  template: `
    <table>
     <tr *ngFor="let skill of skills">{{skill.calcSomething(skill)}}</tr>
    </table>
  `
})
export class SkillsComponent {
  calcSomething(skill) { ... }
}

The problem is, because Angular needs to re-run your function in every change detection cycle, if the function performs expensive tasks, it can be costly.
If the value is not changed dynamically at runtime, a better solution would be to:

    1. Use pure pipes — Angular executes a pure pipe only when it detects a pure change to the input value.

    2. Creates a new property and set the value once, for example:

this.skills = this.skills.map(skill => ( { ...skill, percentage: calcSomething(skill)  } );

4. Disable Change Detection

Imagine that you have a component that depends on data that changes constantly, many times per second.

Updating the user interface whenever new data arrives can be expensive. A more efficient way would be to check and update the user interface every X seconds.

We can do that by detaching the component’s change detector and conducting a local check every x seconds.

@Component({
  selector: 'giant-list',
  template: `
    <li *ngFor="let d of dataProvider.data">Data {{d}}</lig>
  `,
})
class GiantList {
  constructor(private ref: ChangeDetectorRef, private dataProvider: DataProvider) {
    ref.detach();
    setInterval(() => {
      this.ref.detectChanges();
    }, 5000);
  }
}

5. Using Lazy Loading

In my opinion, lazy loading is one of Angular’s most powerful features, and yet the least used.

By default, Webpack will output all your app’s code into one large bundle. Lazy loading gives you the ability to optimize your application load timeby splitting the application to feature modules and loading them on-demand.

Angular makes this process almost transparent.

{
  path: 'admin',
  loadChildren: 'app/admin/admin.module#AdminModule',
}

We can even prevent whole modules from being loaded based on some condition. For example, don’t load the admin module if the user is not an admin. ( see the canLoad guard )


That’s all.