Сервис развивается: тестируем формат, собираем идеи, улучшаем сервис. Есть идеи?

Написать
Войти
Дайджесты
Иллюстрация к статье об оптимизации сборки и разбора бандла в Angular

Почему main.js разрастается до 2 МБ: глубокая оптимизация бандла Angular

Разделение Angular-приложения на ленивые модули не гарантирует компактный файл main.js, если зависимости импортируются через общие модули или ломают tree-shaking. Разбираем работу компилятора Ivy, влияние побочных эффектов в коде и практические методы оптимизации импортов Angular Material.

Почему main.js разрастается до 2 МБ: глубокая оптимизация бандла Angular

Создание современных одностраничных веб-приложений (Single Page Applications) на фреймворке Angular требует внимательного отношения к размеру итоговых файлов JavaScript. Нередко команды разработчиков выполняют базовые рекомендации: настраивают маршрутизацию с использованием ленивой загрузки (lazy loading), выносят изолированные разделы в отдельные модули и включают конфигурацию production-сборки. Однако при анализе сетевых запросов в панелях разработчика выясняется, что первичный бандл main.js продолжает весить от 1.5 до 2 мегабайт, заставляя пользователей мобильных сетей ожидать отрисовки интерфейса в течение нескольких секунд.

Главная причина подобной ситуации заключается в том, что ленивая загрузка маршрутов нарезает на отдельные файлы код самих компонентов, но не защищает от попадания тяжелых сторонних библиотек в первичный бандл старта. Если общие зависимости импортируются неоптимально, они неизбежно удерживаются в основном файле приложения.

Механика ленивой загрузки и скрытые ловушки SharedModule

Ленивая загрузка в Angular работает на уровне ES-модулей с использованием динамического синтаксиса import(). Конфигурация маршрутизатора объявляет точки разделения кода:

const routes: Routes = [
  {
    path: 'admin',
    loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule)
  }
];

При сборке инструментарий выносит AdminModule в отдельный файловый чанк. Однако если внутри AdminModule или любого другого модуля используется общий SharedModule, куда по привычке импортированы библиотеки визуализации графиков, утилиты обработки дат и компоненты UI-систем, вся эта масса кода подключается при старте приложения. В результате main.js разрастается за счет тяжелых библиотек, которые реально требуются только на специфических экранах.

Компиляция в Angular Ivy: генерация инструкций ɵcmp вместо фабрик

Для эффективной оптимизации бандла необходимо понимать, как именно современный компилятор Angular Ivy обрабатывает исходные файлы. Начиная с Angular 9, прежний движок View Engine был полностью заменен на Ivy.

В старой архитектуре View Engine компилятор генерировал для каждого компонента и модуля сторонние файлы фабрик (*.ngfactory.js). Эти файлы содержали жесткие перекрестные ссылки на все декларированные зависимости, из-за чего сборщики кода (Webpack или Rollup) не могли надежно определить и выкинуть неиспользуемые фрагменты.

Ivy применяет локальную компиляцию. Вместо отдельного внешнего файла компилятор генерирует статические функции прямо в теле класса компонента:

  • ɵcmp — инструкция сборки и обновления DOM для компонентов;
  • ɵprov — инструкция внедрения зависимостей для сервисов.

Благодаря локальной компиляции Ivy не требует присутствия всего модуля целиком, если приложению нужен только один конкретный компонент. Это создает основу для глубокого удаления неиспользуемого кода (tree-shaking).

Почему ломается tree-shaking: классы-монстры и побочные эффекты

Удаление мертвых ветвей кода (tree-shaking) опирается на статический анализ ES-модулей. Анализатор проверяет экспорты и импорты, удаляя функции, которые никогда не вызываются. Однако существующие паттерны программирования часто блокируют эту оптимизацию.

Распространенный антипаттерн — использование монолитных классов с набором статических методов:

// Неоптимальный подход: блокирует tree-shaking
export class FormattingUtils {
  static formatDate(d: Date): string { /* ... */ }
  static formatCurrency(val: number): string { /* ... */ }
  static heavyCalculations(data: any): any { /* ... */ }
}

Если компонент импортирует класс FormattingUtils ради одного метода formatDate, сборщик вынужден включить в итоговый бандл весь класс целиком вместе со всеми его методами. Сборщик не имеет права разбирать класс на части.

Для восстановления tree-shaking следует переходить на экспорт точечных независимых функций:

// Оптимальный подход: сохраняет tree-shaking
export function formatDate(d: Date): string { /* ... */ }
export function formatCurrency(val: number): string { /* ... */ }
export function heavyCalculations(data: any): any { /* ... */ }

При таком объявлении сборщик включит в бандл только функцию formatDate, полностью отбросив остальные.

Побочные эффекты, директива PURE и проблемы баррельных экспортов

Tree-shaking полностью останавливается, когда встречает код с побочными эффектами (side effects) — операторы высшего уровня, меняющие глобальное состояние или выполняющие вызовы функций при загрузке файла.

Если в модуле содержится глобальный вызов или вычисление константы через внешнюю функцию, сборщик не может гарантировать безопасность удаления этого файла. Если вызов функции гарантированно чист и не меняет внешнее состояние, ему добавляют аннотацию /*#__PURE__*/:

export const DEFAULT_SETTING = /*#__PURE__*/ calculateDefaultSettings();

Аналогичная проблема возникает при злоупотреблении баррельными файлами (index.ts). Когда index.ts реэкспортирует десятки файлов из папки, импорт одной функции из барреля может случайно подтянуть весь массив связанных модулей. Надежнее выполнять точечные импорты из конкретных файлов.

Оптимизация тяжелых библиотек на примере Angular Material

Библиотека Angular Material часто становится главным источником лишнего объема приложения. Типичная ошибка — создание единого MaterialModule, который импортирует и реэкспортирует абсолютно все модули библиотеки (MatButtonModule, MatTableModule, MatDatepickerModule и т.д.).

Когда такой монолитный модуль импортируется в приложение, сборщик включает в main.js код всех существующих компонентов Material, даже если на странице используется только одна кнопка.

Правильная стратегия — отказываться от единого MaterialModule и подключать только те модули компонентов, которые непосредственно необходимы конкретному ленивому модулю или standalone-компоненту:

@Component({
  standalone: true,
  imports: [MatButtonModule, MatInputModule],
  templateUrl: './feature.component.html'
})
export class FeatureComponent {}

Легковесные токены внедрения зависимостей Lightweight Injection Tokens

В библиотеке Angular Material реализована архитектурная оптимизация — легковесные токены внедрения зависимостей. В прежних версиях фреймворка, если дочерний компонент (например, MatRadioButton) запрашивал в конструкторе родительский компонент (MatRadioGroup), сборщик был обязан включить код MatRadioGroup в бандл, даже если тот не использовался в шаблоне.

Техника Lightweight Injection Tokens заменяет прямую ссылку на класс родительского компонента абстрактным токеном InjectionToken:

export const MAT_RADIO_GROUP = new InjectionToken<MatRadioGroup>('MAT_RADIO_GROUP');

Это позволяет отсекать неиспользуемые контейнерные компоненты из финальной сборки. При разработке собственных библиотек компонентов рекомендуется применять этот же паттерн для ослабления связей между элементами.

Пошаговый алгоритм аудита бандла через webpack-bundle-analyzer

Для проведения точечной оптимизации размером бандла рекомендуется следующая последовательность действий:

  1. Установка анализатора: установите плагин анализа в dev-зависимости проекта:
    npm install --save-dev webpack-bundle-analyzer
    
  2. Сборка со статистикой: выполните сборку приложения в режиме production с сохранением метаданных:
    ng build --configuration production --stats-json
    
  3. Запуск интерактивного анализа: откройте наглядную карту содержимого бандлов в браузере:
    npx webpack-bundle-analyzer dist/your-app-name/stats.json
    
  4. Рефакторинг импортов: выявите наиболее крупные блоки, удалите глобальный MaterialModule, замените тяжелые библиотеки точечными импортами и вынесите специфический код в ленивые маршруты.
  5. Настройка бюджетов: зафиксируйте предельные размеры бандлов в файле angular.json в разделе budgets, чтобы система автоматически предупреждала о превышении объема при последующих сборках.

Подробные рекомендации по настройке сборщика описаны в официальных руководствах Angular по сборке и спецификации Angular Package Format.