Перейти к содержимому

56. Оптимизация сборки

Привет! Яша здесь. Размер бандла напрямую влияет на время загрузки — каждые 100KB JavaScript добавляют ~1 секунду на мобильных устройствах. Сегодня разберём как сделать Angular-приложение максимально компактным 🚀


Окно терминала
ng build --configuration=production

Angular выполняет следующие оптимизации:

  1. AOT компиляция — шаблоны компилируются в TypeScript
  2. Tree shaking — удаление неиспользуемого кода
  3. Minification — сжатие JavaScript и CSS
  4. Dead code elimination — удаление мёртвого кода
  5. Scope hoisting — объединение модулей
  6. Differential loading — отдельные бандлы для ES5/ES2015+

// ❌ JIT (Just-in-Time) — компиляция в браузере
// angular.json: "aot": false
// Браузер получает: Angular компилятор + шаблоны (строки) + код
// Размер: большой, компиляция при каждом запуске
// ✅ AOT (Ahead-of-Time) — компиляция при сборке
// angular.json: "aot": true (по умолчанию с Angular 9)
// Браузер получает: скомпилированный код, без компилятора
// Преимущества:
// - Меньший бандл (нет компилятора ~300KB)
// - Быстрый старт
// - Ошибки шаблонов найдены на этапе сборки
// - Безопаснее (нет eval())
@Component({
// AOT проверяет это при компиляции:
template: `
<h1>{{ titl }}</h1> // ← Ошибка TypeScript: Property 'titl' does not exist
`
})
export class AppComponent {
title = 'Hello';
}

// angular.json — строгий контроль размера
{
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
},
{
"type": "allScript",
"maximumWarning": "1mb",
"maximumError": "2mb"
},
{
"type": "any",
"maximumWarning": "500kb",
"maximumError": "1mb"
}
]
}
}
}

Окно терминала
ng build --stats-json --configuration=production
# 2. Анализ
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"

Что искать в анализаторе:

  • 🔴 moment.js — 65KB gzip → заменить на date-fns или Temporal
  • 🔴 Полный lodash → импортировать только нужные функции
  • 🔴 Дублирующиеся библиотеки (несколько версий)
  • 🟡 Большие компоненты, которые должны быть lazy loaded

// ❌ Импорт всего lodash — 71KB gzip
import _ from 'lodash';
const arr = _.uniq([1, 2, 2, 3]);
// ✅ Импорт только нужной функции — 2KB
import uniq from 'lodash/uniq';
const arr = uniq([1, 2, 2, 3]);
// ✅ Ещё лучше — нативный JavaScript
const arr = [...new Set([1, 2, 2, 3])];
// ❌ moment.js — огромный (не tree-shakeable)
import moment from 'moment';
const date = moment().format('YYYY-MM-DD');
// ✅ date-fns — полностью tree-shakeable
import { format } from 'date-fns';
const date = format(new Date(), 'yyyy-MM-dd');

// angular.json — переход на esbuild
{
"projects": {
"my-app": {
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:application",
"options": {
"browser": "src/main.ts"
}
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server"
}
}
}
}
}
Окно терминала
# Результаты с esbuild (Angular 17+):
# ng build: 15 секунд → 4 секунды (3-4x быстрее!)
# ng serve hot reload: 2 секунды → 0.3 секунды

<!-- index.html — Angular генерирует автоматически -->
<!-- Современные браузеры (ES2017): type="module" -->
<script type="module" src="main-es2017.js"></script>
<!-- Старые браузеры (ES5): nomodule -->
<script nomodule src="main-es5.js"></script>
// tsconfig.json — настройка target
{
"compilerOptions": {
"target": "ES2022", // ← Angular 17+ рекомендует ES2022
"useDefineForClassFields": false
}
}

// ❌ Всё в одном бандле — initial chunk 2MB
@NgModule({
imports: [
AdminModule, // 500KB
ReportsModule, // 400KB
SettingsModule, // 200KB
]
})
export class AppModule {}
// ✅ Lazy loading — initial chunk 200KB
const routes: Routes = [
{
path: 'admin',
loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule),
// → отдельный chunk: admin.chunk.js (500KB) загружается только при /admin
},
{
path: 'reports',
loadChildren: () => import('./reports/reports.module').then(m => m.ReportsModule),
// → отдельный chunk: reports.chunk.js
},
];
// Результат:
// Было: initial 2MB (все пользователи ждут 2MB)
// Стало: initial 200KB + chunks по требованию

<!-- Angular 17+ — встроенная ленивая загрузка -->
<!-- Загрузить когда элемент появился во viewport -->
@defer (on viewport) {
<app-heavy-chart [data]="chartData" />
} @placeholder {
<div class="chart-skeleton" style="height: 300px;">
Загружаю график...
</div>
} @loading (minimum 500ms) {
<app-spinner />
} @error {
<p>Не удалось загрузить компонент</p>
}
<!-- Загрузить когда браузер idle -->
@defer (on idle) {
<app-recommendations />
}
<!-- Загрузить по условию -->
@defer (when isLoggedIn) {
<app-user-dashboard />
}
<!-- Предзагрузить (hover) -->
@defer (on hover; prefetch on idle) {
<app-tooltip />
}

angular.json
{
"configurations": {
"production": {
"sourceMap": false, // ← не публикуй source maps в production!
// Или скрытые source maps (для Sentry без публичного доступа)
"sourceMap": {
"scripts": true,
"styles": false,
"hidden": true // ← файлы создаются, но не ссылаются из бандла
}
}
}
}

// ngx-translate — только нужные части
// ❌
import { TranslateModule } from '@ngx-translate/core'; // весь модуль
// ✅ Standalone import — tree-shaking работает
@Component({
imports: [TranslatePipe], // только pipe
})
// RxJS — правильные импорты
// ❌ Старый стиль
import { Observable } from 'rxjs/Observable';
import { map } from 'rxjs/operators/map';
// ✅ Современный — tree-shakeable
import { Observable, map } from 'rxjs';
// Angular Material — только нужные модули
// ❌
import { MaterialModule } from './material.module'; // всё сразу
// ✅
import { MatButtonModule } from '@angular/material/button';
import { MatInputModule } from '@angular/material/input';

Профилирование производительности сборки

Заголовок раздела «Профилирование производительности сборки»
Окно терминала
# Время сборки с деталями
ng build --verbose
# Профилирование webpack
ng build --stats-json
# Открой stats.json на https://webpack.github.io/analyse/
# Проверить размеры чанков
ls -lh dist/my-app/browser/*.js
# ng build с таймингом
time ng build --configuration=production

Визуализатор размера бандла с анализом оптимизаций: