Menu

Angular Ivy’s internal data structures

March 3rd, 2021

vector character standing looking at a browser with a code symbol on it

I thought it would be useful to dive into Angular’s new Ivy rendering engine’s inner workings. In this article, I would like to stay high level but at the same time provide critical insights into how Ivy internally organizes its data structures to focus on memory performance.

Template, Logical, and Render Trees

When Ivy does rendering, it needs to keep track of three kinds of data: Template, Logical Tree, and Render Tree. These three concepts are shortened for brevity as T, L, and R prefix in many of our data structures.

The template is then parsed version of the source code. It contains instructions for rendering the template in the form of Ivy-instructions and metadata about the component/directive. If you can find it in your source code, then a corresponding field in template data structures will also be present. Template information exists regardless of if the code in it has been executed. For example, a template behind *ngIf would still have this template information even if the condition is false). In Ivy, the template information is stored in TView (as well as TData and TNode) data structures. Together these data structures provided all the static information about the template Ivy needs at runtime. The word “static” is important to differentiate it from the next Ivy concept: the logical-view.

Logical-view (LView) represents instances of a template (TView). We use the word “logical” to highlight how the developer thinks about the application from a logical perspective. A ParentComponent contains a ChildComponent. From a logical perspective, we think about a ParentComponent containing a ChildComponent, hence the “logical” designation. The word “logical” is in contrast to the final concept of the render tree.

The render tree is the actual DOM render tree. It is different from the logical tree above in that the render tree must take content projection into account. Because of this parent/child relationship is not as straightforward as in the logical view.

Let’s look into an example to solidify these concepts.

@Component({
  selector: ‘child’,
  template: ‘<span>I am a child.</span>’
})
class ChildComponent {}
@Component({
  selector: ‘parent’,
  template: ‘
    <div>
      projected content: 
      <ng-content></ng-content>
    </div>
  ’
})
class ParentComponent {}
@Component({
  selector: ‘demo’,
  template: `
    <parent id=”p1">
    <child id=”c1"></child>
    </parent>
    <child id=”c2"></child>
  `
})
class DemoApp {}

After the application is loaded (but before it is bootstrapped), Ivy parses the above code into TView.

Note: more-or-less, A lot of what is described here is lazily performed for performance, also this is pseudo-code rather than the actual execution. The following snippets are pseudo-code.

const tChildComponentView = new TView(
  tData: [
    new TElementNode(‘span’),
    new TTextNode(‘I am a child.’),
  ],
  …,
);
const tParentComponentView = new TView(
  tData: [
    new TElementNode(‘div’),
    new TTextNode(‘projected content: ‘),
    new TProjectionNode(),
  ],
  …,
);
const tDemoAppView = new TView(
  tData: [
    new TElementNode(‘parent’, [‘id’, ‘p1’]),
    new TElementNode(‘child’, [‘id’, ‘c1’]),
    new TElementNode(‘child’, [‘id’, ‘c2’]),
  ],
  …,
)

The next step is to bootstrap (or instantiate) the application. Instantiation involves creating LView instances from the TView.

const lParentComponentView_p1 = new LView(
  tParentComponentView,
  new ParentComponent(…),
  document.createElement(‘div’),
  document.createText(‘projected content: ‘),
);
const lChildComponentView_c1 = new LView(
  tChildComponentView,
  new ChildComponent(…),
  document.createElement(‘span’),
  document.createText(‘I am a child.’),
);
const lChildComponentView_c2 = new LView(
  tChildComponentView,
  new ChildComponent(…),
  document.createElement(‘span’),
  document.createText(‘I am a child.’),
);
const lDemoAppView = new LView(
  tDemoAppView,
  new DemoApp(…),
  document.createElement(‘parent’),
  lParentComponentView_p1,
  document.createElement(‘child’),
  lChildComponentView_c1,
  document.createElement(‘child’),
  lChildComponentView_c2,
)

The above shows the difference between TView and LView. Looking at ChildComponent, there is only one instance of TView, but there are two instances of LView because ChildComponent was used twice. The other key difference is that LView only stores data specific to that instance of the component — such as the component instance and associated DOM nodes. The TView stores information shared across all instances of the component — such as what DOM nodes need to be created.

In the above example, LView is shown as a class (new LView(…).) In reality, we store it as an array ([…].) Storing LView as an array is done for memory performance reasons. Each template will have a different number of DOM nodes and child component/directives, and storing it in the array is the most efficient way.

The implication of using an array for storage is that it is not clear at which location in the array things are stored. TData serves the purpose of describing what is stored at each location in the LView. So LView by itself is insufficient to reason about because it stores values without context as to what these values represent. TView describes what the component needs, but it stores no instance information. By putting LView and TView together, Ivy can access and reason about the values in the LView. LView stores the values, whereas TView stores the meaning for the values in LView.

For simplicity of the example above, the LViews only store DOM nodes. In practice, LView also stores bindings, injectors, sanitizers, and anything else related to the state of the view (with a corresponding entry in the TView/TData.)

A way to think about TView and LView is to relate the concepts to object-oriented programming. TView is like class, and LView is like class instance.

All of the LViews in the system form a tree. For our example, we can visualize it like so.

<#LView>
  <demo>
    <#LView: tDemoAppView, new DemoApp()>
      <parent id=”p1">
        <#LView: tParentComponentView, new ParentComponent()>
          <div>
            projected content: 
            <ng-content></ng-content>
          </div>
        </#LView>
        <child id=”c1">
          <#LView: tChildComponentView, new ChildComponent()>
            <span>
              I am a child.
            </span>
          </#LView>
        </child>
      </parent>
      <child id=”c2">
        <#LView: tChildComponentView, new ChildComponent()>
          <span>
            I am a child.
          </span>
        </#LView>
      </child>
    </#LView>
  </demo>
</#LView>

The above “logical” tree is different from the “render” tree shown below. The main difference is that in the “logical” tree the nodes are kept together, whereas in the “render” tree the nodes are distributed due to content projection.

<demo>
  <parent id=”p1">
    <div>
      projected content: 
      <child id=”c1">
        <span>
          I am a child.
        </span>
      </child>
    </div>
  </parent>
  <child id=”c2">
    <span>
      I am a child.
    </span>
  </child>
</demo>

I hope that this post gives you some insight into how Ivy is optimized for memory consumption. If you like these types of deep dives into the internals of Ivy let us know by leaving a comment. Thanks for reading and thanks for being a part of the Angular community.

The original post Ivy’s internal data structures.