angular
June 10

Defer в Angular

С последними улучшениями контроля потока, Angular v17 вводит впечатляющую и очень полезную функцию: блок defer.

Основная цель блока defer - ленивая загрузка контента. Будь то компонент, директива или канал, если он находится внутри блока defer, Angular загружает его только на основе указанных условий или событий. Это особенно полезно для оптимизации производительности, особенно когда некоторые компоненты не сразу нужны или не видны пользователю.

Рассмотрим этот простой компонент:

@Component({
  selector: 'my-cmp',
  standalone: true,
  template: 'Hi!',
})
class MyCmp { }

Теперь давайте посмотрим, как можно применить блок defer:

@Component({
  standalone: true,
  imports: [MyCmp, FooDirective, BarPipe],
  template: `
    @defer (when isVisible) {
      <my-cmp appFoo/>
      {{ 'foo' | bar }}
    }

    <button (click)="isVisible = true">Load</button>
  `
})
export class AppComponent {
  isVisible = false;
}
В этом примере блок defer сопоставлен с условием when, которое ожидает логическое значение. Компилятор Angular обрабатывает это, разбивая компонент, директиву и пайп на отдельные фрагменты.

Они затем загружаются и рендерятся только тогда, когда isVisible равно true:

Кроме того, при интеграции блока defer с простым HTML-содержимым он приобретает возможности, напоминающие расширенную директиву ngIf. Это становится особенно заметным при совмещении с условием on, о котором мы подробно поговорим позже.

Блок defer можно дополнить блоками @loading, @placeholder и @error для предоставления более полного пользовательского опыта:

@Component({
  template: `
    @defer (when isVisible) {
      <my-cmp />
    }
    @loading {
      Loading...
    } @placeholder {
      Placeholder
    } @error {
      Failed to load dependencies
    }
  `
})
class AppComponent { ... }

  • @loading: Отображает указанное содержимое во время загрузки зависимостей.
  • @placeholder: Отображает указанное содержимое в качестве промежуточного отображения до завершения рендеринга содержимого.
  • @error: Отображает указанное содержимое в случае проблемы с загрузкой зависимостей содержимого.

Условие on

Функциональность отложенной загрузки Angular не ограничивается простой проверкой на булево значение. Вводится универсальное условие "on", которое предлагает целый спектр вариантов для загрузки и рендеринга компонентов. Давайте поглубже разберем его возможности.

Мгновенный Рендеринг с Условием "On Immediate"

@Component({
  standalone: true,
  imports: [MyCmp],
  template: `
    @defer (on immediate) {
      <my-cmp />
    }
  `
})
class AppComponent { }

В приведенном выше коде сочетание блока @defer с условием "on immediate" гарантирует мгновенный рендеринг компонента после его ленивой загрузки.

Рендеринг в простое время с условием «On Idle»

@Component({
  standalone: true,
  imports: [MyCmp],
  template: `
    @defer (on idle) {
      <my-cmp />
    }
  `
})
class AppComponent { }

Здесь компонент загружается и отображается во время простоя браузера, используя API requestIdleCallback.

Отложенная загрузка компонента до определенного взаимодействия пользователя

одна из мощных функций функционала отложенной загрузки Angular. Это особенно полезно в ситуациях, когда вы хотите загружать контент только после того, как пользователь взаимодействует с определенным элементом, например, нажатием кнопки:

@Component({
  standalone: true,
  template: `
    @defer (on interaction(trigger)) {
      <my-cmp />
    }
    @placeholder { 
      Placeholder
    }

    <button #trigger>Trigger</button>
  `
})
class AppComponent { }

В этом примере загрузка и отображение компонента откладываются и будут выполняться только после того, как будет нажата кнопка, обозначенная ссылкой #trigger. До этого взаимодействия блок @placeholder отображает содержимое "Placeholder". Поддерживаемые события взаимодействия включают в себя click, focus, touch и input.

В некоторых ситуациях вы можете не хотеть привязывать блок defer к конкретному элементу. Angular позволяет задерживать загрузку контента на основе взаимодействий с самим заполнителем:

@Component({
  standalone: true,
  template: `
    @defer (on interaction) {
      Main content
    } @placeholder {
      <button>Trigger</button>
    }
  `
})
class AppComponent {}

Отложенная загрузка до наведения мыши

Еще одной универсальной возможностью блока defer в Angular является возможность отложить рендеринг контента до тех пор, пока не будет наведен курсор на определенный элемент:

@Component({
  standalone: true,
  template: `
    @defer (on hover(trigger)) {
      Main content
    } @placeholder {
      Placeholder
    }

    <button #trigger>Trigger</button>
  `
})
class AppComponent { }

Рендеринг по таймеру

Блок @defer в Angular также предоставляет возможность задержать рендеринг контента на основе таймера. Это может быть особенно полезно в сценариях, где требуется поэтапная загрузка нескольких компонентов или введение задержки перед рендерингом компонента, улучшая опыт пользователя контролируемым и тайминговым отображением контента.

@Component({
  standalone: true,
  template: `
    @defer (on timer(1500ms)) {
      Main content
    } @placeholder {
      Placeholder
    }
  `
})
class AppComponent { }

Рендеринг на основе видимой области

Блок @defer в Angular предоставляет встроенный механизм для этого с помощью условия on viewport. Это позволяет разработчикам отложить рендеринг контента до тех пор, пока определенный элемент не войдет в видимую область:

@Component({
  standalone: true,
  template: `
    @defer (on viewport(trigger)) {
      <my-comp />
    } @placeholder {
      Placeholder
    }

    <div #trigger style="margin-top: 1500px">Content</div>
  `
})
class AppComponent { }

Блок @defer (on viewport(trigger)) гарантирует, что компонент будет отображаться только тогда, когда div (определенный с помощью переменной ссылки на шаблон #trigger) попадает в видимую область.

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

Хотя указание явного триггера для условия on viewport может быть полезным, есть сценарии, когда разработчики хотели бы, чтобы отложенный контент загружался на основе видимости самого заполнителя. Блок defer в Angular поддерживает это через неявный механизм триггера области видимости:

@Component({
  standalone: true,
  template: `
    @defer (on viewport) {
      <my-comp />
    } @placeholder {
      <div>Placeholder</div>
    }
  `
})
class AppComponent { }

Блок @defer (on viewport) настроен на отображение компонента, когда содержимое заполнителя попадает в область видимости. В данном случае заполнителем является элемент div с меткой "Placeholder". Этот div действует как неявный триггер для условия области видимости.

Оптимизация с предварительной загрузкой В мире современной веб-разработки производительность играет ключевую роль. Предварительная загрузка, или загрузка ресурсов до их фактического использования, является стратегией, направленной на улучшение производительности. Блок отложенной загрузки Angular (defer block) поддерживает предварительную загрузку ресурсов, обеспечивая готовность ресурсов к использованию и таким образом минимизируя время ожидания и повышая удобство использования для пользователей.

Опция prefetch (предварительная загрузка) находится в естественной симбиозе со всеми функциональными возможностями, которые мы рассмотрели с использованием условий when или on.

Основная предварительная загрузка

В приведенном ниже примере блок @defer комбинируется как с условием when, так и с prefetch. Компонент <my-comp /> будет отображен, когда isVisible равно true. Кроме того, часть компонента будет предварительно загружена, когда prefetchCondition станет true.

@Component({
  standalone: true,
  imports: [MyComp],
  template: `
    @defer (when isVisible; prefetch when prefetchCondition) {
      <my-comp />
    } @placeholder {
      Placeholder
    }
  `
})
class AppComponent {
  isVisible = false;
  prefetchCondition = false;
}

Мгновенная предварительная загрузка

В следующем примере условие prefetch on immediate гарантирует, что часть компонента будет загружена немедленно, даже до того, как это будет необходимо:

@Component({
  standalone: true,
  imports: [MyComp],
  template: `
    @defer (when isVisible; prefetch on immediate) {
      <my-comp />
    } @placeholder {
      Placeholder
    }
  `
})
class AppComponent {
  isVisible = false;
}

Пассивная предварительная загрузка

Здесь условие prefetch on idle гарантирует, что часть компонента будет загружена во время простоя браузера. Это достигается с помощью API requestIdleCallback, которое позволяет планировать задачи во время простоя:

@Component({
  standalone: true,
  imports: [MyComp],
  template: `
    @defer (when isVisible; prefetch on idle) {
      <my-comp />
    } @placeholder {
      Placeholder
    }
  `
})
class AppComponent {
  isVisible = false;
}

Предварительная загрузка на основе таймера

В этом примере условие prefetch on timer(100ms) гарантирует, что компонент будет загружен после задержки в 100 миллисекунд. Это может быть особенно полезно, когда вы хотите ввести небольшую задержку перед предварительной загрузкой ресурсов, позволяя сначала загрузиться другим критическим ресурсам.

@Component({
  standalone: true,
  imports: [MyCmp],
  template: `
    @defer (when isVisible; prefetch on timer(100ms)) {
      <my-comp />
    } @placeholder {
      Placeholder
    }
  `
})
class AppComponent {
  isVisible = false;
}

Viewport-Based Prefetching

@Component({
  standalone: true,
  imports: [MyCmp],
  template: `
    @defer (when isVisible; prefetch on viewport(trigger)) {
      <my-comp />
    } @placeholder {
      Placeholder
    }
  `
})
class AppComponent {
  isVisible = false;
}

Advanced Conditions: Minimum and After

Условия "Minimum" и "After" позволяют более детально управлять длительностью отображения состояния загрузки и заполнителя.

@Component({
  standalone: true,
  imports: [MyCmp],
  template: `
    @defer (when isVisible; prefetch when prefetchTrigger) {
      <my-cmp />
    } 
    @loading (after 100ms; minimum 150ms) {
      Loading
    } 
    @placeholder (minimum 100ms) {
      Placeholder
    } 
    @error {
      Error
    }
  `
})
class AppComponent {
  isVisible = false;
}

after 100ms: Состояние загрузки появляется после задержки в 100 мс. minimum 150ms: Состояние загрузки сохраняется как минимум в течение 150 мс. minimum 100ms: Заполнитель остается видимым как минимум в течение 100 мс.

Примечание: Состояние @loading пропускается, когда ресурсы были предварительно загружены.

Интеграция блока defer в цикл for of

Блок defer можно легко интегрировать в цикл for of, улучшая динамическую отрисовку компонентов в зависимости от условий. Вот демонстрация:

@Component({
  standalone: true,
  imports: [MyCmp],
  template: `
    @for (item of items; track item) {
      @defer (on timer(500ms)) {
        <my-cmp />
      } @placeholder {
        Placeholder for {{ item }}
      }
    }
  `
})
class AppComponent {
  items = [...];
}

В приведенном выше примере для каждого элемента массива items блок defer вводит задержку в 500 мс перед отображением компонента <my-cmp />. Тем временем отображается заполнитель, специфичный для текущего элемента.

Интеграция блока defer с проекцией контента происходит легко и без усилий, что улучшает динамическое включение контента. Вот пример:

@Component({
    selector: 'my-cmp',
    standalone: true,
    imports: [BazComponent],
    template: `
     @defer (when isVisible) {
        <app-baz />
        <ng-content />
     }

     <button (click)="isVisible = true">Show</button>
    `,
  })
  export class MyCmp {
    isVisible = false;
  }


@Component({
  standalone: true,
  imports: [MyCmp],
  template: `
     <my-cmp>
      my content
    </my-cmp>
  `
})
class AppComponent { }

Давайте закончим с практическим примером: шаговый механизм, который предварительно загружает и откладывает загрузку своих шагов во время простоя:

@Component({
  standalone: true,
  imports: [MyCmp],
  template: `
     @if (step === 0) {
        <app-step-one />
        <button (click)="updateStep(1)">Next</button>
     }

     @defer (prefetch on idle) {
        @if (step === 1) {
          <app-step-two />
          <button (click)="updateStep(2)">Next</button>
        }
     }


     @defer (prefetch on idle) {
        @if (step === 2) {
          <app-step-three />
        }
     }
  `
})
class AppComponent {
  step = 0;

  updateStep(step: number) {
    this.step = step;
  }
}