NGRXセレクタを用いた角度低速レンダリングの解法


Twitterで私に従ってくださいNewsletter | もともと公開timdeschryver.dev .
今週、私は角度テンプレートを効率的にレンダリングするいくつかの困難があったプロジェクトに取り組みました.
再レンダリング中に、画面が凍って、レンダリングサイクルが解決するまで無反応になりました.
テンプレート自体はあまり空想的ではありませんでした、それはちょうどテーブルのデータの表のデータのカップルの雇用者の毎月の作業のスケジュールを表示します.各テーブル(1週間)10と30行の間には、合計では、これは、画面上に50と150行の間にあることを意味します.

コードが最適化されなかった間、私は角が視点を与える困難を持つことに少し驚きました.
そういうわけで、私は私の最近の経験でAを出します.いくつかの有用な友人は、このレンダリングの問題を解決するための改善と私のつぶやきに対応しました.
提案のすべてが有効に(そして簡単に)微調整の変更検出サイクルの数を減らすために微調整されます.
例えば、
  • 使うOnPush 代わりに戦略Default 戦略
  • 余分なメソッドの呼び出しを防ぐために、人間の読み取り可能なテキストにプロパティをフォーマットするために純粋なパイプを使用するには
  • 使うtrackBy メソッドを使用すると、*ngFor エレメント
  • 仮想スクロールを使用するには、一度にいくつかの行を表示する
  • しかし、問題を解決するために、私は前に成功に導いた異なるルートで行きました.
    私はコンポーネント/テンプレートの外のロジックのほとんど(またはすべて)を抽出し、それがコンポーネントに達する前にモデルを準備するのが好きです.これは特定の角度APIを知る必要はありません、そして、それはコンポーネントを小さくてきれいにしておきます.私はまた、これをテストし、デバッグを容易にし、おそらく将来的に動作を変更することがわかります.
    “モデルを準備する”と言うことによって私が意味するものを理解するために、最初に問題を引き起こしていたコードを見てみましょう.
    <div *ngFor="let message of criticalMessages().filter(onlyUnique)">{{ message }}</div>
    <div *ngFor="let message of infoMessages().filter(onlyUnique)">{{ message }}</div>
    
    <div *ngFor="let parent of parents">
        <h2>{{ parent.title }}</h2>
        <table>
            <tr *ngFor="let child of getChildRows(parent)" [class]="getRowClass(child)">
                <td><icon [icon]="getIcon(child)"></icon></td>
                <td>{{ formatDate(child) }}</td>
                <td [class]="getNameClass(child)">{{ formatName(child) }}</td>
                <td [class]="getAddressClass(child)">{{ formatAddress(child) }}</td>
                <td>{{ formatDetails(child) }}</td>
                <td>
                    <button *ngIf="canEditChild(child)">Edit</button>
                    <button *ngIf="canDeleteChild(child)">Delete</button>
                </td>
            </tr>
        </table>
    </div>
    
    @Component({})
    export class Component {
        // parent has a list of children
        @Input() parents: Parent[];
        // a message can be critical or info and is bound to a child
        @Input() messages: Message[];
    
        criticalMessages() {
            return messages.filter((message) => message.type === 'critical');
        }
    
        infoMessages() {
            return messages.filter((message) => message.type === 'info');
        }
    
        onlyUnique(value: Message, index: number, self: Message[]) {
            return self.map((message) => message.description).indexOf(message.description) === index;
        }
    
        getChildRows(child: Child) {
            const rows = child.listOne.concat(listTwo);
            return rows.sort((a, b) => (a.date < b.date ? -1 : 1));
        }
    
        getIcon(child: Child) {
            return this.messages
                .filter((message) => message.type === 'critical')
                .some((message) => message.childId === child.id)
                ? 'red-dot'
                : '';
        }
    
        getRowClass(child: Child) {
            // simple logic based on child properties
        }
    
        getNameClass(child: Child) {
            // simple logic based on child properties
        }
    
        getAddressClass(child: Child) {
            // simple logic based on child properties
        }
    
        canEditChild(child: Child) {
            // simple logic based on child properties
        }
    
        canDeleteChild(child: Child) {
            // simple logic based on child properties
        }
    }
    
    あなたが経験豊かな角度開発者であるならば、私はあなたが我々がちょうど見たコードで赤い旗を見つけることができると確信しています.
    同じページ上の誰でも得るために、主な問題はテンプレートの中で使われる多くのメソッドがあるということです.これはおそらく最初に気づきやすいですが、これらのメソッドのロジックが高価になると問題になります.すべての変化検出サイクルに対して、全ての方法が実行される.これは、レンダリングサイクルが完了する前に、単一のメソッドを複数回呼び出すことができます.

    If this is new information, I strongly suggest watching by . It sure helped me when I started my Angular journey.


    我々が問題の原因を知っている今、我々はなぜ我々が変化探知サイクルの数を減らすために我々の絶対的なベストをする必要がある理由を知っています、そして、なぜ、それは最低限にテンプレートでメソッドを保つのが重要です.
    提案された修正を使用する代わりに、データが前処理されるならば、解決を見ましょう.
    テンプレートとコードを見ると、テンプレートを構築するロジックがあることに気付きます.
    例えば、2つの最も重い方法はそれらを分類する前に2つのコレクションを静める方法です、そして、2番目に重い方法はユニークなメッセージだけを表示することになっています.これらの他にも、例えば、複数のプロパティをフォーマットするか、ボタンを表示/非表示にするための、より簡単な方法がありました.
    これらのビューロジックをすべてコンポーネントの外部に移動すると、これらのメソッドは、各変更検出サイクルではなく、一度だけ呼び出されます.
    私が使用しているアプリケーションNgRx , の概念selectors . 私にとって、セレクタはビューロジックを動かす理想的な場所です.
    あなたがNGRXを使用していない場合は心配しないでください、このテクニックはまた、別の状態管理ツールには、純粋なRXJs、および別のフレームワーク全体でも適用できます.
    export const selectViewModel = createSelector(
      // get all the parents
      selectParents,
      // get all the children
      selectChildren,
      // get all the critical and info messages
      selectMessages,
      (parents, children, messages) => {
        // map the child id of critical messages into a set
        // this makes it easy and fast to lookup if a child has a critical message
        const messagesByChildId = messages
          ? new Set(
              messages
                .filter((message) => message.type === 'critical')
                .map((message) => message.childId),
            )
          : new Set();
    
      // use a Set to get unique messages
        const criticalMessages = messages
          ? [
              ...new Set(
                messages
                  .filter((message) => message.type === 'critical')
                  .map((message) => message.description),
              ),
            ]
          : [];
    
      // use a Set to get unique messages
        const infoMessages = messages
          ? [
              ...new Set(
                messages
                  .filter((message) => message.type === 'info')
                  .map((message) => message.description),
              ),
            ]
          : [];
    
        return {
          criticalMessages: criticalMessages,
          infoMessages: infoMessages,
          parents: parents.map((parent) => {
            return {
              title: parent.title,
              children: childrenForParent(parent.listOne, parent.listTwo)
                .map((child) => {
                  return {
                    id: child.id,
                    icon: messagesByChildId.has(child.id) ? 'red-dot' : '',
                    date: child.date,
                    state: child.confirmed ? 'confirmed' : 'pending',
                    edited: child.edited,
                    name: formatName(child),
                    address: formatAddress(child),
                    details: formatDetails(child),
                    canEdit: canEdit(child),
                    canDelete: canDelete(child),
                  };
                })
                .sort(),
            };
          });
        };
      },
    );
    
    // 💡 Tip: create a type for the view model with `ReturnType` and `typeof`
    export type ViewModel = ReturnType<typeof selectViewModel>;
    
    上記のセレクタで、私はそれが簡単に何が起こっているかを確認し、可能な間違いを見つけることがわかります.
    また、コンポーネントがこのリファクタの後にどれだけ簡単になるかを見ることができます.
    もうロジックはコンポーネントにありません、テンプレートはちょうどコレクションの上でループして、(View)モデルの特性を使用します.ニースシンプル.
    <div *ngFor="let message of viewModel.criticalMessages">{{ message }}</div>
    <div *ngFor="let message of viewModel.infoMessages">{{ message }}</div>
    
    <div *ngFor="let parent of viewModel.parents">
      <h2>{{ parent.title }}</h2>
    
      <table>
        <tr *ngFor="let child of parent.children">
          <td><icon [icon]="child.icon"></icon></td>
          <td>{{ child.date }}</td>
          <td [attr.state]="child.state">{{ child.name }}</td>
          <td [attr.state]="child.state" [attr.edited]="child.edited">{{ child.address }}</td>
          <td>{{ child.details }}</td>
          <td>
            <button *ngIf="child.canEdit">Edit</button>
            <button *ngIf="child.canDelete">Delete</button>
          </td>
        </tr>
      </table>
    </div>
    
    そのほかに読むのは簡単ですが、また、角度変化検出メカニズムを心配する必要はありません.
    セレクタ内のロジックは、変更検出サイクルではなくデータが変更されたときのみ実行されます.
    これは非常に効率的になります.
    このテクニックのもう一つの利点は、テストするのが簡単であるということです.
    セレクタをテストするには、projector セレクタのメソッド.
    The projector 正確にこの理由のために存在します.そして、我々がセレクタの中で論理をテストするのを簡単にします.
    これにより、セレクタを固定変数で呼び出し、セレクタの結果をアサートできます.
    これは、コンポーネントのテストを書いたり実行したりするのと比較して、実行したり書き込みたりするのが速いです.
    it('consists of unique messages', () => {
      const result = selectViewModel.projector(
        [{ id: 1, title: 'Parent 1' }],
        [],
        [
          { type: 'critical', message: 'critical message 1' },
          { type: 'critical', message: 'critical message 2' },
          { type: 'critical', message: 'critical message 1' },
          { type: 'info', message: 'info message 1' },
        ],
      );
      expect(result[0].criticalMessages).toEqual(['critical message 1', 'critical message 2']);
      expect(result[0].infoMessages).toEqual(['info message 2']);
    });
    
    あなたがこれをするとき、そして、見解がまだ遅い終わりにあるとき、あなたは以前言及された角度最適化技術にまだ頼ります.私の経験から、私が作成するアプリケーションのために、この「フィックス」は通常十分です、しかし、あなたがあなたのバッグでトリックの余分の一組を持つということを知っていることは常によいです.
    Twitterで私に従ってくださいNewsletter | もともと公開timdeschryver.dev .