Using @defer with a Logical Expression

Angular 17 is slated for release in early November, and one of its headline additions is the deferred loading mechanism (RFC).

Lazy loading is a pattern where web applications fetch resources, such as JavaScript bundles, only at the moment they are actually required. Rather than pulling down every asset at the initial page load, lazy loading postpones the retrieval of non-critical content until the user performs an action, scrolls, or hits a certain point in the viewport.

This strategy improves the overall experience by cutting down the initial load time and allowing users to interact with the application promptly, while secondary portions load in the background. It also trims bandwidth consumption and lightens the server's workload.

Previously, lazy-loading a segment of an Angular application required either the Router or a combination of dynamic import calls with ngComponentOutlet.

Angular 17 elevates this concept considerably. The framework now ships a @defer control block that enables lazy-loading for whatever sits inside it. Importantly, the lazy-loading behavior extends to the entire dependency graph of the block's content: any components, directives, or pipes referenced are also fetched on demand.

This write-up walks you through the core mechanics of Angular 17's lazy loading, covering:

  • How to set up a logical expression that drives when the deferred block renders
  • How to define declarative conditions, such as on hover, to activate rendering, and what trigger options are available
  • How to layer placeholder, loading, or error states via the corresponding @placeholder, @loading, and @error blocks
  • An overview of how prefetching operates

The complete reference implementation can be found at:

https://github.com/gergelyszerovay/angular-17-deferred-loading

The examples here are built with Angular v17.0.0-next.8, leveraging standalone components and Signals. To run the frontend locally, execute yarn run start or npm run start.

Leveraging @defer with a Logical Condition

To illustrate the first scenario, we set up a checkbox that toggles a signal named isCheckedDefer. The signal defaults to false, so the checkbox starts unchecked and the deferred block remains dormant. The following snippet is extracted from the src\app\app.component.html template file:

<div>
  <input #checkboxDefer type="checkbox" [checked]="isCheckedDefer()" (change)="isCheckedDefer.set(checkboxDefer.checked)" id="checkboxDefer"/>
  <label for="checkboxDefer">Open the network tab of the browser's developer tools, then check this checkbox to load the <strong>app-c1</strong> component</label>
</div>
<br>

@defer (when isCheckedDefer()) {
  <app-c1/>
}
@placeholder {
  <span>Placeholder</span>
}
@error {
  <span>Error</span>
}
@loading(minimum 1s) {
  <span>Loading...</span>
}

The directive @defer (when logical_expression) { establishes a deferred block tied to that expression. In this case, we pass the signal invocation isCheckedDefer(), which resolves to a boolean, as the logical trigger.

Within the structure, three supplementary block types coordinate the rendering lifecycle:

  • The @defer block itself: Angular brings this into view once the isCheckedDefer signal flips to true. It hosts a child component, namely <app-c1/>.
  • The @placeholder block: shown immediately, prior to the deferred block being activated.
  • After the trigger fires, Angular begins fetching the deferred content from the server. During this interval, the @loading block is displayed.
  • If the fetching process encounters an error, Angular falls back to the @error block.

Using @placeholder, @loading, and @error is entirely elective; a bare @defer block works independently.

Here’s what happens in practice. On app startup, the isCheckedDefer signal is false; therefore, the deferred block is inactive, and the placeholder's content is what the user sees:

New Angular v17 feature: deferred loading — figure 1

Open the Network tab in the browser's Developer tools and clear the log.

When the checkbox is ticked, the isCheckedDefer signal changes to true. Angular then retrieves the deferred block's content, swaps out the placeholder, and renders whatever is inside the @loading block. We designate a minimum duration on the loading block, ensuring it remains visible for at least one second. There is also an after option, which specifies how long Angular should wait before showing the loading block; if the deferred content arrives within that window, the loading state is skipped entirely.

Consequently, the loading block stays on screen for the full second:

New Angular v17 feature: deferred loading — figure 2

Afterward, the deferred block's content, represented by the <app-a1> component, comes into view:

New Angular v17 feature: deferred loading — figure 3

The Network tab reveals that, upon toggling the checkbox, Angular fetches a fresh chunk containing the deferred block's content:

New Angular v17 feature: deferred loading — figure 4

Next, we reload the application, clear the Network tab, and then disable network requests in the browser:

New Angular v17 feature: deferred loading — figure 5

Checking the box sets isCheckedDefer to true, prompting Angular to load the deferred content. The placeholder is replaced, and the loading block appears.

At this point, the load operation fails due to a connection error, so Angular switches to presenting the @error block:

New Angular v17 feature: deferred loading — figure 6

Employing @defer with a Declarative Trigger

Deferred blocks accept several declarative triggers. Here are the available options:

  • on interaction
  • on hover
  • on idle
  • on timer
  • on viewport

Let's work through each one with a practical example.

Triggering with @defer on interaction

With the on interaction trigger, Angular waits until the user interacts with the @placeholder block before rendering the deferred content. This interaction can take various forms, including clicks, touches, focusing, or keyboard events like keypress:

@defer (on interaction) {
  <span>Clicked</span>
}
@placeholder {
  <span>Placeholder (click on it!)</span>
}
Enter fullscreen mode Exit fullscreen mode

Triggering with @defer on hover

By using the on hover trigger, Angular will render the deferred block once the user's mouse hovers over the associated @placeholder block:

@defer (on hover) {
  <span>Hovered</span>
}
@placeholder {
  <span>Placeholder (hover it!)</span>
}
Enter fullscreen mode Exit fullscreen mode

Triggering with @defer on idle

The on idle trigger instructs Angular to render the block when the browser's main thread becomes idle following the initial page load:

@defer (on idle) {
  <span>Browser has reached an idle state</span>
}
@placeholder {
  <span>Placeholder</span>
}
Enter fullscreen mode Exit fullscreen mode

Triggering with @defer on timer

With the on timer trigger, the deferred content is rendered after a specific duration has passed:

@defer (on timer(5s)) {
  <span>Visible after 5s&lt;/span>
}
@placeholder {
  <span>Placeholder&lt;/span>
}
Enter fullscreen mode Exit fullscreen mode

Triggering with @defer on viewport

Angular will render the on viewport block as soon as the placeholder element scrolls into the visible area of the browser window:

@defer (on viewport) {
  <app-c2 text="The block entered the viewport"/>
}
@placeholder {
  <span>Placeholder</span>
}
Enter fullscreen mode Exit fullscreen mode

After refreshing the application, you can use the inspector to verify whether the content from the @placeholder block is present in the DOM at the initial render:

New Angular v17 feature: deferred loading — figure 7

Next, carry out the following actions:

  • open the Network tab in your browser's developer tools
  • clear any existing entries in the Network tab
  • scroll to the very bottom of the page

You should then observe Angular loading and rendering the @defer block's contents, specifically the <app-c2> component:

New Angular v17 feature: deferred loading — figure 8

Prefetching

In addition to defining a trigger condition, you can also set a separate prefetch condition:

@defer (on interaction; prefetch on hover) {
  <app-c3/>
}
@placeholder {
  <span>Placeholder (hover it, then click on it!)</span>
}
Enter fullscreen mode Exit fullscreen mode

After reloading the app, try these steps:

  • wipe the entries in the "Network" tab
  • move your mouse over the placeholder located in the "Prefetch" section

You'll notice that Angular loads the resources for the @defer block without displaying them, and the @placeholder stays visible in the UI:

New Angular v17 feature: deferred loading — figure 9

Subsequently, upon clicking the placeholder, Angular will render the content that was prefetched, which in this case is the <app-c3> component:

New Angular v17 feature: deferred loading — figure 10

Wrap-Up

This piece walked through several compelling additions in Angular 17: the mechanics behind the new deferred blocks and how to define conditions that control when their content is loaded and placed into the view. Hopefully, the information proved useful for your projects.

In the next installment, the focus shifts to the new control flow syntax, covering @if, @else, @switch, and @case blocks in Angular 17.

As always, don't hesitate to share any thoughts or feedback you might have!

👨‍💻About the Author

I go by Gergely Szerovay, and my day-to-day role is leading a frontend development chapter. My enthusiasm lies in both teaching and learning Angular. Each day I dive into articles, podcasts, conference sessions, and whatever else relates to the framework.

I founded the Angular Addict Newsletter with the goal of curating and sharing the most valuable resources I encounter monthly. Whether you're just starting out or consider yourself an Angular devotee, there's material for you.

Alongside the newsletter, I manage a separate publication fittingly named Angular Addicts. It's a central repository for what I find most instructive and engaging. Interested in contributing as an author? Let me know.

Let's continue learning Angular collectively. You can subscribe right here 🔥

For more Angular insights, connect with me on Substack, Medium, Dev.to, Twitter, or LinkedIn!