はじめに
Angularのパフォーマンス改善で「ChangeDetectionStrategy.OnPush
を指定する」というのをよく見るが、これを設定するとどんな良いことがあるのか、を実際に見える形で確認したかったので試した。
今回はその結果を共有していく。
ChangeDetectionとは
これに関しては@lacolacoさんの記事がとても参考になる(なった)ので、そちらを確認いただきたい。
Angular2のChange Detectionについて
Angular2はいかにしてオブジェクトの変更を監視しているのか
日本語訳:Angular 2 Change Detection Explained
・Change Detectionとはモデルの変更を検知し、UIに反映することである
・Angularはあらゆる非同期処理の後にChange Detectionを行う(Zoneを使って)
・変更を検知する際にはオブジェクトの参照が変わったかどうかが重要である
・Immutableを使えばAngularが変更を検知しやすくなる
・OnPushでChange Detectionをコントロールできる
ChangeDetectionStrategy.OnPush
とはChangeDetectionを走らせる下層のコンポーネントを絞る機能
AngularはデフォルトではどのコンポーネントもChangeDetectionを走らせる設定になっている。
これでは変更する必要のないコンポーネントまでもチェックする必要があり、その数が多いとパフォーマンス低下に繋がる。そこでチェックする必要のないコンポーネントを減らせればその分パフォーマンスの向上に繋がる。その**チェックするかどうかを決められる設定がOnPush
**というわけだ。(正確にはそのコンポーネントからではなく、そこから下のコンポーネントへのチェックを無くせる。)
ChangeDetectionの確認はngDoCheck()
で出来る
A callback method that performs change-detection, invoked after the default change-detector runs. See KeyValueDiffers and IterableDiffers for implementing custom change checking for collections.
実際に確認するアプリの構成
AppComponent
がそれぞれ@Input()
経由でStoreListComponent
, FruitsListComponent
には配列、UserComponent
にはStringの値を渡す。
2層目では配列なら3層目のコンポーネントに1つ1つ要素を渡し、UserComponentであればAppComponent
から受け取った値に文字列を連結させて3層目のコンポーネントに渡す。
AppComponent
, StoreListComponent
でのみ値を変更するイベントを用意し、それぞれどの層のどのコンポーネントにChangeDetectionが走るのかをngDoCheck()
で確認する狙い。
ソースコード
※StoreListとFruitsListおよびそれらのItemコンポーネントは実装がほぼ同じなのでStoreListのみ。
全体のソースコードはこちらから。
app.component
import { ChangeDetectionStrategy, Component, DoCheck } from '@angular/core';
import { Store } from './modules/store-list/store-list.component';
import { Fruits } from './modules/fruits-list/fruits-list.component';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
// changeDetection: ChangeDetectionStrategy.OnPush,
changeDetection: ChangeDetectionStrategy.Default,
})
export class AppComponent implements DoCheck {
public storeList: Store[] = [
{ name: '渋谷店', isFavorite: true },
{ name: '新宿店', isFavorite: false },
{ name: '豊洲店', isFavorite: false },
];
public fruitsList: Fruits[] = [
{ name: 'オレンジ' },
{ name: 'ブドウ' },
{ name: 'バナナ' },
{ name: 'メロン' },
];
public userName = 'John';
public title = 'angular-change-detect';
ngDoCheck(): void {
console.log('AppComponent-DoCheck');
}
/**
* EventだけでChangeDetectionが走るのかを確認する
*/
public consoleLog() {
console.log('AppComponentのconsoleLog()');
console.log('event');
}
/**
* AppComponentの値だけを変更してChangeDetectionが走るのかを確認する
*/
public changeTitle() {
console.log('AppComponentのtitleをchange');
this.title = 'Title updated';
}
/**
* UserComponentの値を変更する
*/
public changeUserName() {
console.log('AppComponentからuserNameをchange');
this.userName = 'Lucy';
}
/**
* StoreListに要素を追加する
*/
public addStore() {
console.log('AppComponentからStoreListに追加');
this.storeList.push({ name: '飯田橋店', isFavorite: false });
}
/**
* StoreListのコピーを作成、それに要素を追加して新しい配列を格納する
*/
public addStoreNewArray() {
console.log('AppComponentから新しいStoreList配列を格納');
const currentStoreList = [...this.storeList];
this.storeList = [...currentStoreList, { name: '飯田橋店', isFavorite: false }];
}
}
<div>{{title}}</div>
<div>
<button (click)="consoleLog()">ConsoleLog</button>
</div>
<div>
<button (click)="changeTitle()">Change Title</button>
</div>
<div>
<button (click)="addStore()">Add Store</button>
</div>
<div>
<button (click)="addStoreNewArray()">Add Store New Array</button>
</div>
<div>
<button (click)="changeUserName()">Change User Name</button>
</div>
<div>
<app-store-list [storeList]="storeList"></app-store-list>
</div>
<div>
<app-fruits-list [fruitsList]="fruitsList"></app-fruits-list>
</div>
<div>
<app-user [userName]="userName"></app-user>
</div>
store-list.component
import { ChangeDetectionStrategy, Component, DoCheck, Input } from '@angular/core';
export interface Store {
name: string;
isFavorite: boolean;
}
@Component({
selector: 'app-store-list',
templateUrl: './store-list.component.html',
styleUrls: ['./store-list.component.scss'],
// changeDetection: ChangeDetectionStrategy.OnPush,
changeDetection: ChangeDetectionStrategy.Default,
})
export class StoreListComponent implements DoCheck {
@Input() storeList: Store[];
constructor() {}
ngDoCheck(): void {
console.log('StoreListComponent-DoCheck');
}
public consoleLog() {
console.log('StoreListからconsoleLog()');
console.log('Event in StoreListComponent');
}
}
<p>Store List</p>
<button (click)="consoleLog()">Add Store List In StoreListComponent</button>
<ul>
<li *ngFor="let store of storeList">
<app-store-list-item [store]="store"></app-store-list-item>
</li>
</ul>
user.component
import { ChangeDetectionStrategy, Component, DoCheck, Input, OnChanges } from '@angular/core';
@Component({
selector: 'app-user',
templateUrl: './user.component.html',
styleUrls: ['./user.component.scss'],
// changeDetection: ChangeDetectionStrategy.OnPush,
changeDetection: ChangeDetectionStrategy.Default,
})
export class UserComponent implements OnChanges, DoCheck {
@Input() userName: string;
public hello = '';
constructor() {}
ngOnChanges(): void {
this.hello = `Hello ${this.userName}`;
}
ngDoCheck(): void {
console.log('UserComponent-DoCheck');
}
}
<p>Hello, {{userName}}</p>
<app-user-child [hello]="hello"></app-user-child>
user-child.component
import { Component, DoCheck, Input } from '@angular/core';
@Component({
selector: 'app-user-child',
templateUrl: './user-child.component.html',
styleUrls: ['./user-child.component.scss'],
})
export class UserChildComponent implements DoCheck {
@Input() hello: string;
constructor() {}
ngDoCheck(): void {
console.log('UserChildComponent-DoCheck');
}
}
<p>user-child: {{hello}}</p>
全コンポーネントがChangeDetectionStrategy.Defaultの場合
赤枠がAppComponent
のClickイベント、青枠がStoreListComponent
のClickイベント。
どちらも全てのコンポーネントのChangeDetectionが走ってしまっていることがわかる。
さっきの図で表すとこんな感じ。
全ての階層のコンポーネントでChangeDetectionが走っている。
ちなみにAppComponentのどのClickイベントでもこうなる。
2層目のコンポーネントでOnPush
を設定した場合
赤枠がAppComponent
のtitle
を変更、青枠がStoreList
に要素を追加、緑枠がStoreList
を新しい配列(新しい参照)にしたとき。
AppComponent
のtitle
変更はStoreList
に影響はないので、StoreListItem
のChangeDetectionが走っていないことが確認できる。
またStoreList
への要素追加ではStoreListItem
のChangeDetectionが走らず、StoreList
の参照を変更した場合はStoreListItem
のChangeDetectionが走っていることが確認できる。
このことから**OnPush
を設定した場合、オブジェクトの値であれば参照が変更されるとChangeDetectionが走ることがわかる。
StoreListのデータに関係のない、または参照が変更されないとOnPush
を設定したコンポーネントよりも下層のコンポーネントのChangeDetectionが走らない。
一方で参照を変更すると、OnPush
を設定したコンポーネントの下層のみにChangeDetectionを絞ることができる。FruitsListItem
にはChangeDetectionが走ってない**。複数のリストを表示する、例えばダッシュボード画面のような構成でかなり効果的なのかも…。
(余談)プリミティブな値を変更した場合
AppComponent
からuserName
の変更イベントを2度走らせてみた。(1度目が赤枠、2度目が青枠。)
1度目は値が変更されたので、UserChildComponent
のChangeDetectionが走っている。
2度目は1度目と同じ値で変更したので値自体は変わらない。するとUserChildComponent
のログが表示されなかったので、UserChildComponent
のChangeDetectionが走ってないことが確認できる。
このことからプリミティブな値は変更が無ければChangeDetectionが走らないことがわかる。
さいごに
今回はChangeDetectionの効果を可視化してみた。
ChangeDetectionStrategy.OnPush
を設定することでChangeDetectionの無駄を省くだけでなく、オブジェクトをImmutableを半強制的に扱わせられることもわかった。(半強制的なのはImmutableでなくても自分でChangeDetectionするメソッドを呼べるため。)
ダッシュボード画面のように複数のオブジェクトを表示するようなときにこのChangeDetectionStrategy.OnPush
を設定すると幸せになるかもしれない。