Understanding Angular's deferrable views - Part. 2

Inside the magic

Matthieu Riegler -

I presented to you in a previous article the basics of deferrable views in Angular. Now that you know what you can do with them, let's in the technical details to extract the performance out of them !

How do they work

Under the hood, the deferrable views are a feature of the angular compiler.

When compiling the templates, the Angular compiler extracts all dependencies used within the @defer block into a separate function and generates a number of dynamic imports to load corresponding JS chunks.

Template compiled
@defer {
  <my-heavy-component />
}
function defer_for_block {
  return [ () => import('./my-heavy-component') ]
}

Basically this @defer block results in a single dynamic import that will be executed when the trigger fires.

Nesting blocks

When there are multiple dependencies in a @defer block they end-up in the same import function (except dependencies located inside of nested @defer blocks).

Template compiled
@defer { // Block A
  @if (option === 'a') {
    <heavy-component-a />
  } @else if (option === 'b') {
    <heavy-component-b />
  } @else {
    @defer { // Block B
      <heavy-component-c />
    }
  }
}
function defer_for_blockA {
  return [
    () => import('./heavy-component-a'),
    () => import('./heavy-component-b')
  ]
}

function defer_for_blockB {
  return [
    () => import('./heavy-component-c')
  ]
}

In this scenario, we have 2 @defer blocks resulting in 2 generated lazy-loading functions.

When the trigger for block BlockA is fired, both chunks would be loaded, but only one of the components would be rendered. The third chunk would only be loaded, once we make it into the last @else block.

So, if all of the components are "heavy", you may want to consider wrapping those components into individual @defer blocks (similar to what we have in the last @else block). The same logic would apply if we replace the @if block with a @switch block.

@for or @defer first ?

Now that we know that nesting has implications, let's compare the differences when using an @for block in combination with a @defer block.

First we need to understand the runtime implications of a @defer block. At runtime, a @defer block's content is represented by an embedded view (as if the content was in an <ng-template>). Next look at 2 possibles combinations :

@defer inside of @for @for inside of @defer
@for (item of items) {
  @defer {
    <my-heavy-component />
  }
}
@defer {
  @for (item of items) {
    <my-heavy-component />
  }
}

In the left example, for each item there will be an embedded view created as a part of @for loop and a nested embedded view created for each @defer block. The number of embedded views would be items.length * 2 and the number of defer block instances would be items.length.

The right example would produce items.length + 1 embedded views and a single defer block instance.

@defer inside of @for @for inside of @defer
  • items.length * 2 embedded views
  • items.length defer block instances
  • items.length embedded views
  • A single defer block instance

So for the cases above, where the contents of the @for loop doesn't have conditions with different "heavy" components, it would make sense to choose the right example where the @defer is outside of the @for loop.

However, if the contents of the @for loop has some extra logic and involves showing different "heavy" components depending on certain conditions, you may want to choose the left example where @defer is inside of the @for loop to wrap a particular component or a group of components.

Now that you know the implications of the usage:

test

If you have any other questions on this great new feature, feel free to reach out to me on Twitter !


Suggestions