LoginSignup
20
17

More than 3 years have passed since last update.

【Angular】`ChangeDetectionStrategy.OnPush`の効果を確認してみる

Last updated at Posted at 2020-02-05

はじめに

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()で出来る

Angular - DoCheck

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()で確認する狙い。
detect-change (1).png

ソースコード

※StoreListとFruitsListおよびそれらのItemコンポーネントは実装がほぼ同じなのでStoreListのみ。
全体のソースコードはこちらから。


app.component

このコンポーネントで各層のコンポーネントの値を書き換える。
consoleから確認できるよう各Clickイベントでログを表示するようにした。
app.component.ts
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 }];
    }
}
app.component.html
<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

子コンポーネントでEventが発生してもルートコンポーネントからChangeDetectionが走るのかどうかを確認するため、ここだけClickイベントを実装している。
store-list.component.ts
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');
    }
}
store-list.component.html
<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

UserComponentでは@Input()の値が変更されたとき挨拶の文言を結合して子コンポーネントに送るようにしている。
user.component.ts
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');
    }
}
user.component.html
<p>Hello, {{userName}}</p>
<app-user-child [hello]="hello"></app-user-child>


user-child.component
user-child.component.ts
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');
    }
}
user-child.component.html
<p>user-child: {{hello}}</p>

全コンポーネントがChangeDetectionStrategy.Defaultの場合

赤枠がAppComponentのClickイベント、青枠がStoreListComponentのClickイベント。
どちらも全てのコンポーネントのChangeDetectionが走ってしまっていることがわかる。
スクリーンショット 2020-02-06 1.39.03.png

さっきの図で表すとこんな感じ。
全ての階層のコンポーネントでChangeDetectionが走っている。
ちなみにAppComponentのどのClickイベントでもこうなる。
detect-change (2).png

2層目のコンポーネントでOnPushを設定した場合

赤枠がAppComponenttitleを変更、青枠がStoreListに要素を追加、緑枠がStoreListを新しい配列(新しい参照)にしたとき。
AppComponenttitle変更はStoreListに影響はないので、StoreListItemのChangeDetectionが走っていないことが確認できる。
またStoreListへの要素追加ではStoreListItemのChangeDetectionが走らず、StoreListの参照を変更した場合はStoreListItemのChangeDetectionが走っていることが確認できる。
このことからOnPushを設定した場合、オブジェクトの値であれば参照が変更されるとChangeDetectionが走ることがわかる。
スクリーンショット 2020-02-06 1.56.58.png
StoreListのデータに関係のない、または参照が変更されないとOnPushを設定したコンポーネントよりも下層のコンポーネントのChangeDetectionが走らない。
detect-change (3).png
一方で参照を変更すると、OnPushを設定したコンポーネントの下層のみにChangeDetectionを絞ることができる。FruitsListItemにはChangeDetectionが走ってない。複数のリストを表示する、例えばダッシュボード画面のような構成でかなり効果的なのかも…。
detect-change (4).png

(余談)プリミティブな値を変更した場合

AppComponentからuserNameの変更イベントを2度走らせてみた。(1度目が赤枠、2度目が青枠。)
1度目は値が変更されたので、UserChildComponentのChangeDetectionが走っている。
2度目は1度目と同じ値で変更したので値自体は変わらない。するとUserChildComponentのログが表示されなかったので、UserChildComponentのChangeDetectionが走ってないことが確認できる。
このことからプリミティブな値は変更が無ければChangeDetectionが走らないことがわかる。
スクリーンショット 2020-02-06 2.14.56.png

さいごに

今回はChangeDetectionの効果を可視化してみた。
ChangeDetectionStrategy.OnPushを設定することでChangeDetectionの無駄を省くだけでなく、オブジェクトをImmutableを半強制的に扱わせられることもわかった。(半強制的なのはImmutableでなくても自分でChangeDetectionするメソッドを呼べるため。)
ダッシュボード画面のように複数のオブジェクトを表示するようなときにこのChangeDetectionStrategy.OnPushを設定すると幸せになるかもしれない。

参考

20
17
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
17