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, что разбираются здесь
@defer: отложенные представления

0

1

Войти