Модуль 5 · урок 9 з 10
Продуктивність
«Without UI lag or memory leak issues» — остання вимога цілі. Тисяча рядків у DOM це вже багато, і будь-яка дрібна недбалість тут множиться на тисячу. Добра новина: усі інструменти в тебе вже є з модулів 3 і 4, лишається застосувати їх у правильному порядку.
Що коштує дорого
| Проблема | Чому дорого | Рішення |
|---|---|---|
| Метод у шаблоні | Виконується на кожній перевірці для кожного рядка | computed, пайп або готове поле |
Немає trackBy | Кожне оновлення перебудовує всі рядки | [trackBy] по id |
Немає OnPush | Кожен рядок перевіряється щоразу | OnPush усім компонентам |
| Мутація масиву | Оновлення не видно або перемальовується все | Незмінні оновлення |
| Тисяча рядків у DOM | Сам браузер повільно рендерить і скролить | Пагінація або віртуальний скрол |
| Складна комірка | Компонент на кожну комірку × 1000 | Простий шаблон, компонент лише де треба |
Найдорожча помилка: робота в шаблоні
// ❌ 1000 рядків × 4 виклики × кожна перевірка змін
<td>{{ formatDate(row.dueDate) }}</td>
<td>{{ userName(row.assigneeId) }}</td>
<td>{{ isOverdue(row) ? 'Прострочено' : '' }}</td>
<td>{{ getStatusLabel(row.status) }}</td>
// ✅ підготувати дані один раз, у сторі
readonly rows$ = combineLatest([this.tasks$, this.users$]).pipe(
map(([tasks, users]) => tasks.map(t => ({
...t,
assigneeName: users.find(u => u.id === t.assigneeId)?.name ?? '—',
dueLabel: formatDate(t.dueDate),
overdue: new Date(t.dueDate) < new Date(),
}))),
);
users.find(...) усередині map по задачах дає складність
«задачі × користувачі»: 1000 × 18 це вісімнадцять тисяч порівнянь на кожне оновлення.
Лікується словником: const byId = new Map(users.map(u => [u.id, u])) —
і пошук стає миттєвим. Це рівно та ката indexBy з модуля 2.
Віртуальний скрол
import { ScrollingModule } from '@angular/cdk/scrolling';
<cdk-virtual-scroll-viewport itemSize="48" class="viewport">
<table>
<tr *cdkVirtualFor="let row of rows(); trackBy: trackById">
<td>{{ row.code }}</td>
</tr>
</table>
</cdk-virtual-scroll-viewport>
У DOM тримається лише те, що видно, плюс невеликий запас. Тисяча рядків перетворюється на двадцять елементів.
- Рядків тисячі й пагінація не підходить за задумом.
- Висота рядка однакова й відома.
- Потрібен нескінченний скрол.
- Пошук по сторінці браузером (Ctrl+F) працює лише по видимому.
- Липкі шапки й обʼєднані комірки стають складнішими.
- Рядки різної висоти потребують окремої стратегії.
- З пагінацією зазвичай простіше й достатньо.
Для нашої цілі віртуалізація не потрібна: лід просить саме клієнтську пагінацію, а 25 рядків на сторінці рендеряться миттєво. Але знати про неї варто — це очевидне питання-продовження на дзвінку.
Порядок дій, коли таблиця гальмує
- Відкрий профайлер Angular DevTools і подивись, скільки компонентів у циклі.
Усе дерево — бракує
OnPush. - Увімкни Paint flashing. Блимає весь список при зміні одного рядка — бракує
trackByабо десь мутація. - Пошукай у шаблоні виклики методів і нечисті пайпи.
- Перевір складність обчислень: чи немає
findусерединіmap. - І лише тепер думай про віртуалізацію.
Про витоки
// таблиця сама відписується від DataSource — тут витоку не буде
<table mat-table [dataSource]="rows()">
// а от це — класика
ngOnInit(): void {
this.sort.sortChange.subscribe(…); // ❌ живе після знищення таблиці
this.paginator.page.subscribe(…); // ❌ те саме
}
Події MatSort і MatPaginator — це звичайні нескінченні потоки.
Правило з модуля 4 діє й тут: або takeUntilDestroyed(), або обробники прямо
в шаблоні через (matSortChange) і (page).