← 解説「描画の無駄を減らす — 変わった部分だけ描き直す」に戻る
サンプルコードで身につける: 描画の無駄を減らす — 変わった部分だけ描き直す
解説で学んだ概念を、5つの具体例で確認します。素朴な例から実務寄りの例へと進みます。
1track $index が並べ替えで全行を作り直す
trackの指定が実質的に効いていない例と修正版です。track $indexは「n番目の行」を識別子にするため、並べ替えると全行の中身が変わったと判定されます。一意なIDに変えるだけで、DOMの組み替えだけで済むようになります。
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-product-list',
// ── 遅い書き方: track $index ──
// '@for (p of products(); track $index) { ... }'
// 並べ替えると「0番目の中身が変わった」…と全行が再描画される
//
// ── 速い書き方: 一意な ID を track にする ──
template:
'<button (click)="sortByPrice()">価格順に並べ替え</button>' +
'@for (p of products(); track p.id) {' +
' <p>{{ p.name }}: {{ p.price }}円</p>' +
'}',
})
export class ProductListComponent {
products = signal([
{ id: 1, name: 'コーヒー', price: 500 },
{ id: 2, name: '紅茶', price: 400 },
]);
sortByPrice() {
// id が同じ行は DOM が再利用され、順番だけが入れ替わる
this.products.update((list) =>
[...list].sort((a, b) => a.price - b.price)
);
}
}
2毎回新しいオブジェクトを作ると track が無力化する
trackを正しく書いていても、更新のたびに全要素を新しいIDで作り直すと効果が消える例です。サーバーから再取得したデータでも、IDが安定していれば行は再利用されます。「trackの値が更新をまたいで安定しているか」が確認ポイントです。
import { Component, signal } from '@angular/core';
type Task = { id: number; title: string; done: boolean };
@Component({
selector: 'app-task-list',
template:
'@for (task of tasks(); track task.id) {' +
' <p>{{ task.title }}</p>' +
'}',
})
export class TaskListComponent {
tasks = signal<Task[]>([
{ id: 1, title: '見積書を送る', done: false },
{ id: 2, title: '議事録を書く', done: false },
]);
// ── 遅い書き方: 1件直すだけなのに全件のIDを振り直す ──
// completeAllSlow() {
// this.tasks.set(
// this.tasks().map((t, i) => ({ ...t, id: Date.now() + i }))
// ); // id が毎回変わる = track には「全部別物」に見える
// }
// ── 速い書き方: id は変えず、変わった項目だけ差し替える ──
complete(id: number) {
this.tasks.update((list) =>
list.map((t) => (t.id === id ? { ...t, done: true } : t))
);
// id が安定しているので、該当行以外の DOM はそのまま残る
}
}
3シグナル+OnPushで変わった部品だけ再描画する
既習のシグナルとOnPushが描画性能にどう効くかを1つの親子構成で確認する例です。親のカウンターを更新しても、シグナルを読んでいない子は再描画されません。ゾーンレス環境ではこの「必要な部品だけ更新」が既定の動きになります。
import { ChangeDetectionStrategy, Component, input, signal } from '@angular/core';
@Component({
selector: 'app-price-tag',
changeDetection: ChangeDetectionStrategy.OnPush,
// input が変わったときだけ再描画される子コンポーネント
template: '<span>{{ price() }}円</span>',
})
export class PriceTagComponent {
price = input.required<number>();
}
@Component({
selector: 'app-shop',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [PriceTagComponent],
template:
'<p>閲覧数: {{ views() }}</p>' +
'<button (click)="views.set(views() + 1)">閲覧+1</button>' +
'<app-price-tag [price]="price()" />',
})
export class ShopComponent {
views = signal(0);
price = signal(500);
// views を更新しても price は変わらないので、
// PriceTagComponent は再描画されない(DevToolsのプロファイラで確認できる)
}
4テンプレートのメソッド呼び出しを computed に直す
テンプレートから重い関数を直接呼ぶと、変更検知のたびに再計算される問題の対処例です。computedに移すと、依存するシグナルが変わったときだけ再計算され、結果はキャッシュされます。一覧の絞り込みや集計で特に効きます。
import { Component, computed, signal } from '@angular/core';
type Item = { id: number; name: string; price: number };
@Component({
selector: 'app-cart',
// ── 遅い書き方: 変更検知のたびに totalOf() が再実行される ──
// template: '<p>合計: {{ totalOf() }}円</p>'
// totalOf() { return this.items().reduce((s, i) => s + i.price, 0); }
//
// ── 速い書き方: computed は items が変わったときだけ再計算 ──
template:
'<p>合計: {{ total() }}円</p>' +
'@for (item of items(); track item.id) {' +
' <p>{{ item.name }}</p>' +
'}',
})
export class CartComponent {
items = signal<Item[]>([
{ id: 1, name: 'コーヒー豆', price: 1200 },
{ id: 2, name: 'ドリッパー', price: 800 },
]);
// 結果はキャッシュされ、items が変わるまで使い回される
total = computed(() =>
this.items().reduce((sum, item) => sum + item.price, 0)
);
}
5Angular DevTools プロファイラで再描画を確認する
描画最適化の効果を目で確かめる手順です。プロファイラを録画すると、変更検知のたびにどのコンポーネントが何ミリ秒かかったかが表示されます。直す前に録画して問題を確認し、直した後に同じ操作で録画して比べます。
# ── Angular DevTools プロファイラの使い方 ──
# 0. Chrome拡張「Angular DevTools」をインストールしておく
# (開発モードのアプリで動く。DevTools に Angular タブが増える)
# 1. DevTools → Angular タブ → Profiler を開く
# 2. 「記録」を押し、気になる操作(一覧更新など)を行って停止
#
# 読み方:
# ・棒グラフ1本 = 1回の変更検知サイクル。高い棒が重い更新
# ・棒をクリックすると、コンポーネントごとの所要時間が見える
# ・ボタンを1回押しただけなのに大量のコンポーネントが
# 毎回チェックされていたら、無駄な再描画のサイン
# (track の見直しや OnPush + シグナル化の候補)
#
# 使いどころ: 修正の前後で同じ操作を録画して比較する。
# 差がなければその最適化は不要だった、と判断できるのが最大の価値