Angular
@defer: отложенные представления
Дашборд грузит тяжёлый график, комментарии и виджет чата, хотя пользователь видит только верх страницы. Весь их JavaScript попадает в начальный бандл и тормозит первый рендер. Блок @defer переворачивает это: содержимое и его зависимости выгружаются в отдельный чанк и подтягиваются только тогда, когда они реально нужны - при появлении в области видимости, по клику или в простое браузера. Ленивая загрузка становится частью шаблона.
- Графики и дашборды: тяжёлая библиотека визуализации грузится по on viewport
- Комментарии и отзывы: подгрузка по прокрутке вниз, не блокируя основной контент
- Чат-виджеты: загрузка по on interaction при первом клике на иконку
- Тяжёлые редакторы: WYSIWYG или код-редактор грузятся по on hover над кнопкой
- Аналитика и трекеры: некритичный код по on idle, когда браузер свободен
Предварительные знания
- Встроенный control flow: @if, @for, @switch в шаблонах Angular
- Понимание разбиения бандла на чанки и ленивой загрузки
- Базовое представление о Core Web Vitals: LCP, INP
Ленивая загрузка спускается в шаблон
До @defer ленивая загрузка в Angular означала разбиение по роутам через loadComponent или ручную динамическую import-загрузку. Внутри одной страницы отложить часть UI было трудно. Angular 17 (ноябрь 2023) принёс блок @defer как часть нового встроенного control flow: декларативный синтаксис прямо в шаблоне, который сам выносит содержимое в отдельный чанк и грузит по триггеру. К Angular 21 @defer стал зрелым инструментом и основой инкрементальной гидрации.
Блок @defer и сопутствующие блоки
Блок @defer оборачивает часть шаблона, которую нужно загрузить лениво. Angular находит компоненты, директивы и пайпы внутри блока, выносит их в отдельный JavaScript-чанк и загружает только по триггеру. По умолчанию (без явного триггера) блок грузится on idle - когда браузер свободен.
@placeholder показывается до начала загрузки и обязателен для большинства триггеров. @loading отображается во время загрузки чанка, параметр minimum не даёт спиннеру мигнуть на быстром соединении. @error появляется, если загрузка чанка провалилась (например, сеть отвалилась).
Содержимое @placeholder попадает в начальный бандл, поэтому оно должно быть лёгким: скелетон или текст, а не другой тяжёлый компонент. Смысл @defer теряется, если плейсхолдер сам тянет много кода.
Какой блок показывается до того, как началась загрузка отложенного содержимого?
Триггеры загрузки
Триггер задаётся в скобках after @defer и решает, когда начнётся загрузка. Каждый триггер отвечает на вопрос когда: браузер свободен, блок виден, был клик или ховер, прошло время или нужно сразу.
| Триггер | Когда срабатывает | Типичный случай |
|---|---|---|
| on idle | Браузер в простое (requestIdleCallback) | Некритичный контент, поведение по умолчанию |
| on viewport | Блок попал в область видимости | Контент ниже сгиба: комментарии, графики |
| on interaction | Клик или нажатие клавиши на плейсхолдере | Чат-виджет, раскрывающийся блок |
| on hover | Курсор навёлся на плейсхолдер | Тяжёлая подсказка, превью |
| on timer | По прошествии заданного времени | on timer(3s) для отложенного баннера |
| immediate | Сразу после рендера, но отдельным чанком | Разгрузка начального парсинга без задержки показа |
Триггеры on viewport, on interaction и on hover по умолчанию слушают элемент плейсхолдера. Можно указать другой элемент по template reference variable: on viewport(trigger). Несколько триггеров комбинируются через точку с запятой и срабатывают по первому из них.
Триггеры on можно дополнить условием when с булевым выражением: @defer (on viewport; when ready()). Блок загрузится, как только сработает любой из триггеров. when полезен для логики, завязанной на сигнал состояния.
Какой триггер @defer лучше подходит для секции комментариев в самом низу длинной страницы?
Prefetch и требования к зависимостям
prefetch отделяет загрузку чанка от его показа: код можно скачать заранее, а отрисовать позже. Это убирает задержку в момент, когда блок реально понадобится. Например, скачать чат по on idle, но показать только on interaction.
Здесь чанк чата скачивается, пока браузер свободен (prefetch on idle), а отрисовывается только при первом клике (on interaction). Пользователь не ждёт сетевой загрузки в момент клика, потому что код уже в памяти.
Чтобы зависимость действительно ушла в ленивый чанк, она должна использоваться только внутри @defer. Если тот же компонент упомянут где-то ещё в шаблоне напрямую, он попадёт в основной бандл, и выигрыша @defer не будет. Все зависимости внутри @defer обязаны быть standalone.
Ошибка, отменяющая выигрыш @defer
Зависимость, общая с не-defer частью шаблона, не выносится в ленивый чанк
Компонент HeavyChart используется и внутри @defer, и в шапке страницы напрямую. Поскольку он нужен вне @defer, сборщик включает его в основной бандл. @defer для него больше не уменьшает начальный размер, хотя синтаксически всё корректно.
Почему компонент внутри @defer может всё равно оказаться в основном бандле?
Связь с другими темами
@defer стоит на control flow и питает инкрементальную гидрацию:
- Control flow — @defer - блок того же встроенного синтаксиса, что @if и @for
- Инкрементальная гидрация — Использует триггеры @defer, чтобы гидрировать части серверного HTML по мере надобности
Итог
- @defer выносит своё содержимое и его зависимости в отдельный чанк и грузит по триггеру
- Триггеры: on idle, on viewport, on interaction, on hover, on timer, immediate, плюс prefetch для предзагрузки
- @placeholder показывается до загрузки, @loading во время неё, @error при сбое загрузки
- Зависимости внутри @defer должны быть standalone и не использоваться вне блока, иначе они попадут в основной бандл
- @defer уменьшает начальный бандл и улучшает LCP, перенося некритичный код на потом
Связанные уроки
- ng-05-control-flow — @defer - блок встроенного control flow, поэтому синтаксис @if и @for нужно знать заранее
- ng-35-incremental-hydration — Инкрементальная гидрация работает на тех же триггерах @defer, что разбираются здесь