64. Оптимизация производительности
53. Оптимизация производительности ⚡
Заголовок раздела «53. Оптимизация производительности ⚡»Привет! Яша здесь. Производительность Angular-приложений — это не магия, а конкретные техники. Сегодня разберём всё: от OnPush до bundle analysis. После этого урока твои приложения будут летать 🚀
Почему Angular может тормозить
Заголовок раздела «Почему Angular может тормозить»Angular по умолчанию использует Default стратегию change detection — она проверяет ВСЁ дерево компонентов при любом событии. Представь 1000 компонентов на странице — и каждый клик запускает 1000 проверок.
Default CD: каждое событие → проверить все компоненты сверху внизOnPush CD: только при изменении @Input или async pipe → намного меньше работыOnPush: первый и главный оптимизатор
Заголовок раздела «OnPush: первый и главный оптимизатор»import { Component, Input, ChangeDetectionStrategy } from '@angular/core';
// ✅ OnPush — компонент проверяется только когда:// 1. Изменилась ссылка на @Input// 2. Сработал EventEmitter (@Output)// 3. async pipe получил новое значение// 4. markForCheck() вызван вручную@Component({ selector: 'app-user-card', template: ` <div class="card"> <h3>{{ user.name }}</h3> <p>{{ user.email }}</p> </div> `, changeDetection: ChangeDetectionStrategy.OnPush, // ← КЛЮЧЕВАЯ СТРОКА})export class UserCardComponent { @Input() user!: { name: string; email: string };}
// ❌ Default — всегда проверяется@Component({ selector: 'app-user-card', template: `...`, // changeDetection: ChangeDetectionStrategy.Default (по умолчанию)})export class SlowUserCardComponent { @Input() user!: { name: string; email: string };}Правило: OnPush везде по умолчанию. Используй иммутабельные данные.
ChangeDetectorRef: ручное управление
Заголовок раздела «ChangeDetectorRef: ручное управление»import { ChangeDetectorRef, ChangeDetectionStrategy } from '@angular/core';
@Component({ changeDetection: ChangeDetectionStrategy.OnPush})export class DashboardComponent implements OnInit { data: DashboardData | null = null;
constructor(private cdr: ChangeDetectorRef) {}
ngOnInit(): void { // Данные из WebSocket — Angular не знает об изменениях this.websocket.on('data', (newData) => { this.data = newData; this.cdr.markForCheck(); // ← сообщаем Angular: "проверь меня!" }); }
// detach/reattach — хирургический инструмент pauseUpdates(): void { this.cdr.detach(); // полностью отключить CD для компонента }
resumeUpdates(): void { this.cdr.reattach(); this.cdr.detectChanges(); // мгновенная проверка }}trackBy в ngFor: обязательно!
Заголовок раздела «trackBy в ngFor: обязательно!»@Component({ template: ` <!-- ❌ Без trackBy — каждый рендер пересоздаёт ВСЕ DOM-элементы --> <div *ngFor="let item of items">{{ item.name }}</div>
<!-- ✅ С trackBy — пересоздаются только изменившиеся элементы --> <div *ngFor="let item of items; trackBy: trackById"> {{ item.name }} </div>
<!-- Современный синтаксис (Angular 17+) --> @for (item of items; track item.id) { <div>{{ item.name }}</div> } `})export class ListComponent { items: Item[] = [];
trackById(index: number, item: Item): number { return item.id; // ← уникальный идентификатор }}Pure Pipes: кэширование вычислений
Заголовок раздела «Pure Pipes: кэширование вычислений»// ✅ Pure pipe — пересчитывается только при изменении входных данных@Pipe({ name: 'filterByStatus', pure: true, // по умолчанию true})export class FilterByStatusPipe implements PipeTransform { transform(items: Item[], status: string): Item[] { console.log('Pipe called!'); // вызовется только при реальном изменении return items.filter(item => item.status === status); }}
// ❌ Плохо — метод в шаблоне вызывается при КАЖДОЙ проверке!@Component({ template: ` <!-- Вызывается при каждом CD — даже если items не изменились --> <div *ngFor="let item of getFilteredItems()">{{ item.name }}</div>
<!-- ✅ Pipe кэшируется Angular --> <div *ngFor="let item of items | filterByStatus:'active'">{{ item.name }}</div> `})export class BadComponent { getFilteredItems(): Item[] { return this.items.filter(i => i.status === 'active'); // ← вызов при каждом CD! }}Lazy Loading модулей
Заголовок раздела «Lazy Loading модулей»const routes: Routes = [ { path: 'dashboard', loadChildren: () => import('./dashboard/dashboard.module') .then(m => m.DashboardModule), }, // Standalone компоненты (Angular 14+) { path: 'settings', loadComponent: () => import('./settings/settings.component') .then(c => c.SettingsComponent), },];Стратегии предзагрузки
Заголовок раздела «Стратегии предзагрузки»import { PreloadAllModules, RouterModule } from '@angular/router';import { QuicklinkStrategy, QuicklinkModule } from 'ngx-quicklink';
// PreloadAllModules — загружает все модули после первой загрузкиRouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules,})
// Кастомная стратегия — только помеченные маршрутыexport class SelectivePreloadingStrategy implements PreloadingStrategy { preload(route: Route, load: () => Observable<any>): Observable<any> { return route.data?.['preload'] ? load() : of(null); }}
const routes: Routes = [ { path: 'popular-feature', loadChildren: () => import('./feature/feature.module').then(m => m.FeatureModule), data: { preload: true }, // ← предзагрузить },];NgOptimizedImage
Заголовок раздела «NgOptimizedImage»import { NgOptimizedImage } from '@angular/common';
@Component({ imports: [NgOptimizedImage], template: ` <!-- ✅ NgOptimizedImage: lazy loading, correct sizes, LCP optimization --> <img ngSrc="assets/hero.jpg" width="800" height="400" priority>
<!-- ✅ С responsive srcset --> <img ngSrc="https://cdn.example.com/photo.jpg" width="400" height="300" [ngSrcset]="'400w, 800w, 1200w'" sizes="(max-width: 400px) 100vw, 50vw">
<!-- ❌ Обычный img без оптимизации --> <img src="assets/hero.jpg"> `})export class HeroComponent {}Bundle Analysis с webpack-bundle-analyzer
Заголовок раздела «Bundle Analysis с webpack-bundle-analyzer»npm install --save-dev webpack-bundle-analyzer
# 2. Собрать с генерацией статистикиng build --stats-json
# 3. Запустить анализаторnpx webpack-bundle-analyzer dist/my-app/browser/stats.json
# Или добавить скрипт в package.json# "analyze": "ng build --stats-json && npx webpack-bundle-analyzer dist/my-app/browser/stats.json"// angular.json — настройка бюджетов сборки{ "budgets": [ { "type": "initial", "maximumWarning": "500kb", "maximumError": "1mb" }, { "type": "anyComponentStyle", "maximumWarning": "2kb", "maximumError": "4kb" } ]}Виртуализация списков (CDK)
Заголовок раздела «Виртуализация списков (CDK)»import { ScrollingModule } from '@angular/cdk/scrolling';
@Component({ imports: [ScrollingModule], template: ` <!-- Рендерит только видимые элементы — идеально для 10k+ items --> <cdk-virtual-scroll-viewport itemSize="50" style="height: 400px"> <div *cdkVirtualFor="let item of items; trackBy: trackById" style="height: 50px; display: flex; align-items: center;"> {{ item.name }} </div> </cdk-virtual-scroll-viewport> `})export class VirtualListComponent { items = Array.from({ length: 10000 }, (_, i) => ({ id: i, name: \`Item \${i}\` }));}Lighthouse и Core Web Vitals
Заголовок раздела «Lighthouse и Core Web Vitals»// Улучшение LCP (Largest Contentful Paint)// 1. Используй NgOptimizedImage с priority для hero-изображений// 2. Предзагрузка критических шрифтов в index.html
// Улучшение FID/INP (Interaction to Next Paint)// 1. Деферируй тяжёлые компоненты@defer (on viewport) { <app-heavy-chart />} @placeholder { <div class="chart-skeleton"></div>}
// 2. Используй Web Workers для CPU-intensive задач// 3. Избегай long tasks > 50ms в главном потоке
// Улучшение CLS (Cumulative Layout Shift)// 1. Всегда указывай width/height для изображений// 2. Резервируй место для асинхронного контентаЧеклист производительности 📋
Заголовок раздела «Чеклист производительности 📋»// ✅ Архитектура// - OnPush везде// - Иммутабельные данные (spread, Object.assign)// - trackBy для всех ngFor// - Pure pipes вместо методов в шаблонах// - Lazy loading всех feature-модулей
// ✅ Рендеринг// - NgOptimizedImage для всех изображений// - @defer для off-screen контента// - CDK Virtual Scroll для больших списков// - Избегать сложной логики в ngOnChanges
// ✅ Бандл// - Анализ webpack-bundle-analyzer// - Бюджеты в angular.json// - Tree shaking (избегать side-effect imports)// - esbuild вместо Webpack (Angular 17+)
// ✅ Загрузка// - SSR/SSG для критических страниц// - HTTP2 push для критических ресурсов// - Service Worker для кэшированияPlayground 🎮
Заголовок раздела «Playground 🎮»Визуализация разницы Default vs OnPush change detection: