Angular IDPStrong Junior
Модулі / Грид / Продуктивність
🔥 0

Модуль 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 рядків на сторінці рендеряться миттєво. Але знати про неї варто — це очевидне питання-продовження на дзвінку.

Порядок дій, коли таблиця гальмує

  1. Відкрий профайлер Angular DevTools і подивись, скільки компонентів у циклі. Усе дерево — бракує OnPush.
  2. Увімкни Paint flashing. Блимає весь список при зміні одного рядка — бракує trackBy або десь мутація.
  3. Пошукай у шаблоні виклики методів і нечисті пайпи.
  4. Перевір складність обчислень: чи немає find усередині map.
  5. І лише тепер думай про віртуалізацію.

Про витоки

// таблиця сама відписується від DataSource — тут витоку не буде
<table mat-table [dataSource]="rows()">

// а от це — класика
ngOnInit(): void {
  this.sort.sortChange.subscribe(…);        // ❌ живе після знищення таблиці
  this.paginator.page.subscribe(…);         // ❌ те саме
}

Події MatSort і MatPaginator — це звичайні нескінченні потоки. Правило з модуля 4 діє й тут: або takeUntilDestroyed(), або обробники прямо в шаблоні через (matSortChange) і (page).