Angular
Ленивая загрузка маршрутов
Приложение растёт, и однажды начальный бандл раздувается до мегабайтов: пользователь ждёт загрузки кода админки и настроек, хотя открыл всего лишь главную. Ленивая загрузка решает это на уровне маршрута. Вместо прямой ссылки на компонент маршрут получает функцию динамического импорта, и сборщик автоматически выносит этот код в отдельный чанк. Чанк скачивается только тогда, когда пользователь реально переходит на маршрут. Начальная загрузка становится тонкой, а тяжёлые разделы платят за себя сами в момент захода.
- Раздел админки: код панели управления грузится только у админов при заходе на /admin, обычные пользователи его не качают
- Angular CLI: динамический import() в loadComponent автоматически порождает отдельный чанк при сборке
- Крупные дашборды: каждый функциональный модуль выносится в свой чанк, начальный бандл остаётся компактным
- Стратегии предзагрузки: PreloadAllModules дотягивает чанки в фоне после отрисовки главной, делая последующие переходы мгновенными
- Мобильный трафик: тонкий первый бандл ускоряет первую отрисовку, что напрямую влияет на метрики Core Web Vitals
Предварительные знания
- Базовая маршрутизация: provideRouter, Routes, router-outlet
- Динамический import() в JavaScript и понятие чанка сборки
- Standalone-компоненты и их экспорт
loadComponent: ленивый компонент
В нестрогой конфигурации маршрут ссылается на компонент напрямую: component: AdminComponent. Такой импорт статичен, поэтому компонент попадает в начальный бандл. Ленивая версия заменяет прямую ссылку на loadComponent с функцией динамического импорта. Сборщик видит import() и автоматически выносит код в отдельный чанк, который скачается лишь при переходе на маршрут.
Функция в loadComponent вызывается маршрутизатором в момент активации маршрута. Она возвращает промис с компонентом. Браузер скачивает чанк, маршрутизатор подставляет компонент в outlet. Если на маршрут так и не зайдут, чанк вообще не загрузится, экономя трафик и время старта.
Ленивая загрузка работает именно благодаря статически анализируемому import(). Сборщик распознаёт литерал импорта на этапе компиляции и режет код на чанки. Если путь импорта собрать из переменной, статический анализ ломается и деление не происходит.
Почему компонент, подключённый через loadComponent: () => import('./admin.component'), не попадает в начальный бандл?
loadChildren: ленивый раздел
loadComponent грузит один компонент. Когда лениво нужен целый раздел с собственными вложенными маршрутами, применяют loadChildren: функция динамически импортирует файл с массивом дочерних маршрутов. Весь раздел вместе со своими компонентами уезжает в отдельный чанк.
- loadComponent — Лениво грузит один standalone-компонент. Подходит для отдельного экрана без вложенных маршрутов.
- loadChildren — Лениво грузит массив дочерних маршрутов из отдельного файла. Подходит для целого раздела со своей внутренней навигацией.
В standalone-эпоху loadChildren импортирует не NgModule, а массив маршрутов (Routes). Это упрощает структуру: раздел - это файл с конфигурацией маршрутов и набор standalone-компонентов, без модуля-обёртки.
Нужно лениво загрузить целый раздел /admin с тремя вложенными экранами и внутренней навигацией. Что подходит лучше?
Стратегии предзагрузки и баланс
У ленивости есть оборотная сторона: при первом заходе на маршрут пользователь ждёт скачивания чанка. Предзагрузка сглаживает это. Стратегия дотягивает ленивые чанки в фоне после того, как главная страница уже отрисована и интерактивна. Когда пользователь дойдёт до маршрута, чанк уже в кэше, и переход мгновенный.
| Стратегия | Поведение | Когда уместна |
|---|---|---|
| NoPreloading (по умолчанию) | Чанк грузится только при заходе на маршрут | Много редких разделов, важен минимальный трафик |
| PreloadAllModules | Все ленивые чанки дотягиваются в фоне после старта | Несколько разделов, важна мгновенность переходов |
| Своя стратегия | Предзагружаются только помеченные маршруты (например, по data) | Точечный контроль: грузить вероятные разделы, пропускать редкие |
Баланс простой. Чем мельче деление, тем тоньше начальный бандл и быстрее первая отрисовка, но тем чаще пользователь ждёт чанк при переходе. Предзагрузка переносит это ожидание в фон. Разумная отправная точка: лениво грузить крупные и редкие разделы, а часто посещаемые - предзагружать после старта.
PreloadAllModules дотягивает все чанки и на медленной мобильной сети может съесть трафик ради разделов, куда пользователь не зайдёт. Если разделов много и они разные по востребованности, своя стратегия предзагрузки по флагу в route data точнее, чем загрузка всего подряд.
В чём основной компромисс между ленивой загрузкой без предзагрузки и стратегией PreloadAllModules?
Связь с другими темами
Урок про разбиение кода по маршрутам. Дальше - выбор способа рендеринга каждого:
- Angular Router: маршруты — Та же конфигурация Routes, но компонент подгружается динамическим импортом
- Режимы рендеринга маршрутов — Каждому маршруту можно назначить prerender, SSR или client-side рендеринг
Итог
- loadComponent: () => import('...') лениво грузит один standalone-компонент, сборщик выносит его в отдельный чанк
- loadChildren лениво грузит группу маршрутов из отдельного файла, удобно для целого раздела приложения
- Деление кода по маршрутам уменьшает начальный бандл: пользователь качает только код открытого экрана
- Стратегии предзагрузки (withPreloading) дотягивают чанки в фоне после первой отрисовки, ускоряя переходы
- Баланс: агрессивное деление уменьшает первый бандл, но добавляет задержку при первом заходе на маршрут; предзагрузка сглаживает это
Связанные уроки
- ng-22-router-intro — Ленивая загрузка - это та же конфигурация Routes, но с динамическим импортом вместо прямой ссылки на компонент
- ng-25-render-modes — Деление по маршрутам - предпосылка для назначения каждому маршруту своего режима рендеринга