Back to the homepage
Angular

How to use Angular’s defer block to improve performance?

It’s crucial to ensure a pleasant user experience – especially when users are waiting for something to happen. In such instances, having a placeholder or a loading indicator can be helpful. However, what if the component is heavy or the user has a slow internet connection? We could improve this by controlling the prefetching of assets. Let’s dive in.

What is Angular’s defer block?

Angular’s deferrable views provide a useful feature known as the defer block, allowing you to postpone loading specific content or large components until it becomes necessary in a declarative way. The defer block serves as a container for the deferred content and offers multiple triggers to control the timing and manner in which the content is loaded.

Identifying routing configuration issues 

Let’s picture this scenario: You’re in the middle of building a large web application with many features and views.

export const routes: Routes = [
   {
      path: ‘yellow’,
      component: YellowComponent
   },
   {
      path: ‘cyan’,
      component: CyanComponent
   },
   {
      path: ‘purple’,
      component: PurpleComponent
   }
]

Each of these features has a large and complex routing configuration. Angular defaults to eager loading all of them and then… the performance issue arises.

The following routing configuration is causing some issues related to the core web vitals. Angular recognizes these three features and, during compilation, eagerly loads all of them, resulting in everything being bundled into the main.js file. On the diagram they are represented by the colors yellow, cyan, and purple. While the application may work fine, increasing initial bundle size may negatively impact the core web vitals and performance.

Improving Angular Performance with Code Splitting and Lazy-Loading

There might be some issues with violating the LCP and TTFB metrics, which can be resolved through code splitting. Fortunately, Angular offers some API to help with this, such as using the loadComponent function during routing configuration. 

export const routes: Routes = [
   {
      path: ‘yellow’,
      loadComponent: () => 
         import(‘./yellow.component’).then(it => it.YellowComponent)
   },
   {
      path: ‘cyan’,
      loadComponent: () =>
         import(‘./cyan.component’).then(it => it.CyanComponent)
   },
   {
      path: ‘purple’,
      loadComponent: () => 
      import(‘./purple.component’).then(it => it.PurpleComponent)
   }
]

By providing the path and returning a promise, Angular can lazy-load the necessary components without causing performance issues by creating separate chunks. 

This helps to decrease initial bundle size and improve the overall performance.

Splitting a feature

Imagine a situation where you have a page or feature which has many components. Every feature may have a different flow and every user may follow its own unique flow. This means dividing a big feature into smaller lazy loaded chunks can be useful for optimizing the user experience based on different flows within the application. For example, if User A follows Flow A and activates chunk cyan-1, and User B follows Flow B and activates chunk cyan-2, you can ensure that each user only loads the necessary components for their specific flow, improving performance and reducing unnecessary loading times.

To create this kind of chunks we can create a component instance:

const componentInstance = await import(‘./cyan.component’)
   .then(it => it.CyanComponent);

and pass it to createComponent method of ViewContainerRef:

this.vcr.createComponent(componentInstance)

But such a solution has several downsides. For heavy components or slow network connections, it’s a common practice to provide a loading indicator so that the user is aware that something is loading. It’s also important to handle any errors that may occur in the loading phase. We need to pay attention to manual cleanup to not create several instances of the component. Additionally, it’s better not to import the component into the imports array, as this will eagerly load it instead of lazy loading it.

Deferred Loading and Chunk Creation

To address these issues, we can use a defer block. This allows us to defer the loading of specific parts of large components until they become necessary. By using the defer block, we can improve the user experience and ensure that the website is running as efficiently as possible.

In the case of our setup of three lazily loaded components (yellow, cyan and purple), by putting the CyanComponent inside a defer block to lazy-load it:

`cyan-component.html`

@defer {
   <div>
<!-- Deferred part of the template - ->
(...)
</div>
}
<div>
	<!-- Eagerly loaded part of the template - ->
	(...)
</div>

the component tree becomes  annotated with a defer block, which is identified by Angular during compilation. 

As a result, the following chunks are generated: yellow, purple, chunk cyan-1, chunk-cyan-2.

Additionally, we can control when to fetch and display the component based on a boolean expression:

@defer (when isVisible) {
   <app-cyan />
}

As soon as isVisible becomes true, Angular retrieves and exhibits the content of the CyanComponent. However, when isVisible switches back to false, the CyanComponent is not reverted back to the placeholder, so it is a one-time operation.

Improving user experience with placeholders

Guiding the user in the form of placeholders can be very helpful in improving the user experience. For instance, in a user registration form, having placeholders for fields like first name and last name can help users understand what information is expected from them. Similarly, in the case of defer block in an angular application Angular, having a placeholder can help the user understand what is happening and avoid any confusion, as it is visible until the component gets rendered.

@defer (when isVisible) {
   <app-cyan />
} @placeholder {
  <span> Will be overwritten with the cyan’s content </span>
}

It is also important to note that avoiding flickering issues can further improve the user experience. Providing control over the placeholder, such as setting a minimum duration for it to remain visible, can be very helpful in this regard. By doing so, the user will have a smoother experience, and the application or website will appear more professional and polished.

@defer (when isVisible) {
   <app-cyan />
} @placeholder(minimum 500ms) {
  <span> Will be overwritten with the cyan’s content </span>
}

With defer block we delay loading of the component, so we should have a loading indicator to let a user know that something is happening. Content inside @loading block is visible while fetching a lazy chunk. To further increase user experience we can define the amount of time after content is displayed and minimum amount of time it is visible on screen using optional parameters.

@defer (when isVisible) {
   <app-cyan />
} @placeholder(minimum 500ms) {
  <span> Will be overwritten with the cyan’s content </span>
} @loading(after 100ms; minimum 1s) {
  <span> Loading... </span>
}

If something goes wrong and a loading error has occurred, we can display a message for the user using @error block.

@defer (when isVisible) {
   <app-cyan />
} @placeholder(minimum 500ms) {
  <span> Will be overwritten with the cyan’s content </span>
} @loading(after 100ms; minimum 1s) {
  <span> Loading... </span>
} @error {
  <span> Oops! Failed to download </span>
}

Exploring Various Trigger Types in Angular

Angular is a powerful framework that offers a variety of interaction types to create engaging user experiences. Understanding how these triggers work is essential for developers who want to create robust and functional Angular applications. Each of these has its own unique set of characteristics and behaviors that can be implemented to enhance user engagement and functionality in Angular applications.

While the `when` keyword specifies a boolean condition, the `on` keyword specifies a trigger condition using available triggers:

idle triggers deferred loading once the browser has reached an idle state (detected using requestIdleCallback API) – so as soon as the browser doesn’t have any heavy lifting task. This is the default behavior.

immediate fetches the chunk right away during template execution.

interaction triggers the deferred block when user interacts with the element through the click or keydown event

hover triggers when the user hovers over an element, using the mouseenter and focusin browser events under the hood.

viewport triggers when element is visible on viewport – behind the scenes Angular uses the Intersection Observer API

timer triggers element’s fetch and display after specified amount of time

Multiple triggers can be defined at once (also using on and when keyword together) in one statement. To resolve it Angular always uses logical operator OR.

@defer (on viewport; when condition) {
   <app-cyan />
} @placeholder {
   <span> Will be overwritten with the cyan’s content </span>
}

Understanding implicit and explicit triggers

For the interaction, hover and viewport triggers we can distinguish between implicit and explicit trigger definition. Taking interaction trigger as example:

Implicit interaction triggers the defer block when the user clicks on its placeholder, as it is only visible element for the user:

@defer (on interaction) {
   <app-cyan />
} @placeholder {
   <span> Click here to load </span>
}

On the other hand, an explicit interaction defines an element, by providing its template variable reference, which should be clicked to trigger loading of the component inside the defer block. It is worth mentioning that there is no need to declare a click event handler.

<button #trigger> Click to load </button>

@defer (on interaction(trigger)) {
   <app-cyan />
}

Prefetching Components

If the component is very heavy or the user has a slow connection, we may need even more control over the deferred loading process. To address this issue, Angular offers a declarative approach to prefetch chunks of data using the “prefetch” keyword. Developers can prefetch data to improve user experience using any combination of the triggers described so far.

@defer (on “action”; prefetch on “action”) {
   <app-cyan />
}

@defer (on “action”; prefetch when “boolean expr”) {
   <app-cyan />
}

@defer (when “boolean expr”; prefetch on “action”) {
   <app-cyan />
}

@defer (when “boolean expr”; prefetch when “boolean expr”) {
   <app-cyan />
}

Advancements in Angular: enhancing user experience and loading performance

Angular is an ever-evolving framework that is constantly improving to enhance the user experience and optimize loading performance. Despite the absence of a direct option to target specific display ports, the Angular development team is tirelessly working to provide developers with better tools and capabilities. 

By combining the deferred block and preloading, developers can optimize their applications’ performance based on specific requirements and offer a seamless and enjoyable user experience. This framework provides developers with everything they need to create cutting-edge applications and inspires them to push the boundaries of what’s possible.

 

Co-workes:
Miłosz Rutkowski
Damian Maduzia

About the author

Fanis Prodromou

I am a full-stack web developer with a passion for Angular and NodeJs. I live in Athens-Greece, and I have worked in many big companies. During my 14 years of coding, I have developed vast experience in code quality, application architecture, and application performance.

Being aware of how rapidly computer science and the tech aspects evolve, I try to stay up to date by attending conferences and meetups, by studying and trying new technologies. I love sharing my knowledge and help other developers.

“Sharing is Caring”

I teach Angular in enterprise companies via Code.Hub institute, I write articles and create YouTube videos.

Don’t miss anything! Subscribe to our newsletter. Stay up-to-date with the latest trends, tips, meetups, courses and be a part of a thriving community. The job market appreciates community members.

Leave a Reply

Your email address will not be published. Required fields are marked *