20 May 2024
5 min

Optimization techniques – Reusing views

Reusing views

Sometimes you may need to implement a show-then-hide UI workflow that repeatedly re-renders the same component or a view template.
In the example below, the button a user click on determines the component that will be rendered on the screen,
while the other component will be hidden:

Image alt

A straightforward implementation of the above logic would be to use ngIf or ngSwitch directive to render
the required component and hide the others:

@Component({
  selector: 'host-rv-alt',
  template: `
    <button (click)="type = 'A'">Show A</button>
    <button (click)="type = 'B'">Show B</button>

    <div class="section">
      <ng-container [ngSwitch]="type">
        <a-rv *ngSwitchCase="'A'"></a-rv>
        <b-rv *ngSwitchCase="'B'"></b-rv>
      </ng-container>
    </div>
  `,
})
export class HostRvAltComponent {
  type = null;
}

@Component({ selector: 'a-rv', ... })
export class ARvComponent {}

@Component({ selector: 'b-rv', ... })
export class BRvComponent {}

The problem with such approach is that we might have tens or even hundreds of different components that we need to alternate between.
Using the ngIf or ngSwitch directive will make a template unwieldy, hard to understand and non performant.
It's possible that we may not be able to use these directives if the component to render is determined dynamically in runtime
and loaded lazily.

A better alternative to using those directives would be
ViewContainerRef.
This service allows to attach and render components dynamically.
To render a component, we'll need to define a view container in the template, e.g. on the ng-container element, obtain its reference with @ViewChild and add the component using the
createComponent
method:

@Component({
	selector: "host-rv",
	template: `
		<button (click)="show('A')">Show A</button>
		<button (click)="show('B')">Show B</button>
		<div class="section" style="margin: 20px">
			<ng-container #vc></ng-container>
		</div>
	`,
})
export class HostRvAltComponent {
	@ViewChild("vc", { read: ViewContainerRef, static: true }) vc;

	show(type) {
		this.vc.clear();
		this.vc.createComponent(type === "A" ? ARvComponent : BRvComponent);
	}
}

Notice how clean the template looks like compared to the one where we used ngSwitch directive.
However, even with the view container API, we still have one big problem remaining –
an undesirable consequence of destroying and re-creating views
each time a button is clicked.

Here’s the recording that shows it:

A debugger is paused inside the
createLView
function that’s used by Angular to create a new instance of a component view.
Everytime we click on a button, the function gets executed, which shows that Angular repeatedly destroys and re-creates the view.

What's interesting is that the program execution enters the function twice.
First execution creates an LView for the component that renders text on the screen – either ARvComponent or BRvComponent.
When the code gets inside the function for the second time, Angular creates a view to host LView created during the first pass.

Angular implements optimization techniques to speed up LView creation through the
blueprint
LView:

export interface TView {
  blueprint: LView;
  ...
}

This LView instance is pretty shallow as we can see on the screenshot below:

Image alt

So Angular still needs to add lots of stuff to that shallow view inside the createLView function:

function createLView(parentLView, tView, context, flags, ...) {
  const lView = tView.blueprint.slice();
  lView[HOST] = host;
  lView[FLAGS] = flags | LViewFlags.CreationMode;\
  lView[PARENT] = lView[DECLARATION_VIEW] = parentLView;
  lView[CONTEXT] = context;
  ...

  return lView;
}

Ideally we should avoid this churning of the views altogether.

This could be achieved by creating an instance of a view only once, caching it somewhere and then reusing it when needed. A
view container
provides an API to do exactly this.

To attach an existing view to a view container we can use the insert method
and to detach the view without destroying it we can use the detach method.
Here’s the interface for the methods:

abstract class ViewContainerRef {
  createEmbeddedView<C>(templateRef: TemplateRef<C>, ...): EmbeddedViewRef<C>;
  createComponent<C>(componentType, ...): ComponentRef<C>;

  detach(index?: number): ViewRef|null;
  insert(viewRef: ViewRef, index?: number): ViewRef;
  move(viewRef: ViewRef, currentIndex: number): ViewRef;
  remove(index?: number): void;
}

Calling the remove method will destroy the view, so we should only use it when we don’t need to use the view anymore.

Before we explore how we can use these methods for optimization, let’s briefly discuss the topic of a ViewRef.
You can see that it's used extensively in the API definition above.

ViewRef as a wrapper for LView

We discussed
LView data structure many times throughout this course. However, LView is part of the framework internals and not designed to be exposed to developers. To interact with an LView instance, Angular implements a higher level concept known as ViewRef:

export class ViewRef<T>
	implements
		viewEngine_EmbeddedViewRef<T>,
		viewEngine_InternalViewRef,
		viewEngine_ChangeDetectorRef_interface {}

It’s exposed to developers through the view container API and the ChangeDetectorRef injectable.
When you inject a ChangeDetectorRef into the component constructor, effectively you inject a ViewRef that wraps the underlyng LView. So when you call detectChanges on the ChangeDetectorRef, you’re calling detectChanges on the underlying LView:

export class ViewRef<T> implements viewEngine_ChangeDetectorRef_interface {
	detectChanges(): void {
		detectChangesInternal(
			this._lView[TVIEW],
			this._lView,
			this.context as unknown as {}
		);
	}
}

Besides ChangeDetectorRef, a ViewRef also represents 3 types of views that a view container returns or takes as parameters:

export interface InternalViewRef extends ViewRef {}
export abstract class EmbeddedViewRef<C> extends ViewRef {}
export class RootViewRef<T> extends ViewRef<T> {}

Those views are:

To render a component we can use a component class reference:

const componentRef = viewContainerRef.createComponent(ARvComponent);

The method returns
componentRef
which provides an access to a component class instance as well as related objects:

export class ComponentRef<T> extends AbstractComponentRef<T> {
	override instance: T;
	override hostView: ViewRef<T>;
	override changeDetectorRef: ChangeDetectorRef;
	override componentType: Type<T>;
}

Once we’ve got the view, it then can be attached to a view container using insert method.
The other view you no longer want to show can be removed and preserved using detach method.
The insert method on a view container takes a ViewRef.
As you can see, the ViewRef is exposed through the property hostView.
So when rendering the component we use this property to get the ViewRef and then attach it to a view container:

const compRef = viewContainer.createComponent(ARvComponent);
...
// remove all views (components and embedded views) from the view container
viewContainer.detach();
...
// render the component
viewContainer.insert(compRef.hostView);

Notice again that we’re using detach method instead of clear or remove to preserve the view for later reuse.

To create an embedded view we use a TemplateRef and createEmbeddedView method:

const embeddedView = viewContainer.createEmbeddedView(templateRef);
...
// remove all views (components and embedded views) from the view container
viewContainer.detach();
...
// render the component
viewContainer.insert(embeddedView);

The overall relationship between entities and the view container API can be illustrated like this:

Image alt

All right, let’s see now how we can implement our optimization.

Preserve and re-insert the view

To reuse the view, we’ll first need to create its instance and keep a reference to it.
To render a component, we’ll insert the view into the container.
When a different component will need to be rendered,
we’ll simply detach previous views and insert the one corresponding the component.

Here’s the way to do:

export class HostRvComponent {
	@ViewChild("vc", { read: ViewContainerRef, static: true }) vc;
	types = [ARvComponent, BRvComponent];

	show(type) {
		this.vc.detach();

		const componentRef = this.vc.createComponent(cmp);
		this.vc.insert(componentRef.hostView);
	}
}

Pretty straightforward. We’re using
createComponent
method implemented by the view container. The method instantiates a component view
and returns a viewRef wrapped into the ComponentRef. When inserting, we’re accessing the viewRef through the
hostView
property.

So now the only thing left to do is to save the view and re-use it when needed:

export class HostRvComponent {
	@ViewChild("vc", { read: ViewContainerRef, static: true }) vc;
	cache = new Map();
	types = [ARvComponent, BRvComponent];

	show(type) {
		this.vc.detach();

		const cmp = this.types[type];

		if (this.cache.has(cmp)) {
			const ref = this.cache.get(cmp);
			this.vc.insert(ref.hostView);
			return;
		}

		const componentRef = this.vc.createComponent(cmp);
		this.cache.set(cmp, componentRef);
	}
}

If we now run the code and check the execution flow, we can clearly see that the createLView function
is only executed when the view is created for the first time:

This optimization may not have a big impact if all you render is a single component.
But you can imagine a lot more elaborate setup with lots of nested views.
This is often the case with dashboard widgets. Let’s now see one of such use cases.

Dashboard

Suppose we have lots of widgets in a dashboard and we want to optimize them.
Each widget might have a view that contains hundreds of DOM elements:

Image alt

One way to optimize is to only show the content of the widget when the widget becomes visible.
We could use a common virtual scroll approach, but that would destroy and re-create
the view inside the widget similar to how ngIf and ngSwitch does it.

Since we now know we could preserve the view, we could implement something like a virtual scroll without destroying the view:

In the implementation above the main content of a widget is wrapped in an element with the .container-element CSS class.
As soon as the widget becomes visible, we can see that the number of elements increases, because we render a widget’s content into the DOM.
Once the widget is hidden, the view is removed from the DOM, and the number of elements goes down.

To implement the following optimization, we’ll need to use an intersection observer to know when to show and hide the widget.
A handler that will react to the visibility changes will notify the affected widgets so that they
can hide the view by detaching or show it through insertion.
Between these operations the view will be kept in the widget’s cache and cleaned up on the widgets destruction.

Let’s start with rendering widget items. A content of each widget is defined using the template part:

@Component({
	selector: "d-rv",
	template: `
		<div *ngFor="let item of items" class="wrapper">
			<div [container]="{item, tpl}"></div>
		</div>
		<ng-template #tpl let-item="item">
			<span class="container-content">{{ item.name }}</span>
		</ng-template>
	`,
})
export class DRvComponent {
	items = [
		{ name: "Home 1", container: null },
		{ name: "Home 2", container: null },
		{ name: "Home 3", container: null },
		{ name: "Home 4", container: null },
	];
}

To actually render the template we’ll implement this directive:

@Directive({
	selector: "[container]",
})
export class ContainerDirective {
	@Input("container") params;
	embeddedViewRef;
	item;

	constructor(public viewContainerRef: ViewContainerRef) {}

	ngOnInit() {
		this.item = this.params.item;
		this.viewContainerRef.createEmbeddedView(this.params.tpl, { item });
	}
}

When we now run the code, we’ll see 4 widgets rendered on the screen with the content inside.
Hence when we query the number of elements we get 4, no surprise here:

Image alt

This is a non-optimized behavior – although only 2 items are visible,
the internal content of each widget in the .container-content DOM wrapper is rendered for all 4 items.

Adding show and hide functionality

We could start our optimization journey by adding show/hide functionality to the directive that renders the content of a widget.
Here’s the implementation:

@Directive({
	selector: "[container]",
})
export class ContainerDirective {
	@Input("container") params;
	embeddedViewRef;
	item;

	constructor(
		public element: ElementRef,
		public viewContainerRef: ViewContainerRef
	) {}

	ngOnInit() {
		this.item = this.params.item;
		this.embeddedViewRef = this.params.tpl.createEmbeddedView({
			item: this.params.item,
		});
		this.viewContainerRef.insert(this.embeddedViewRef);
	}

	show(): void {
		if (this.embeddedViewRef) {
			this.viewContainerRef.insert(this.embeddedViewRef);
		}
	}

	hide(): void {
		this.viewContainerRef.detach();
	}
}

This time instead of creating an embedded view through a view container, we create the view using TemplateRef and save it.
To show the content, we insert the view into the container. When the content needs to be hidden, we simply detach it.

To test the implementation, let’s show and hide the view inside the setTimeout callback like this:

@Directive({...})
export class ContainerDirective {
  @Input('container') params;
  embeddedViewRef;
  item;

  constructor(public element: ElementRef, public viewContainerRef: ViewContainerRef) {
    setTimeout(() => this.hide(), 2000);
    setTimeout(() => this.show(), 4000);
  }
}

All working good:

Image alt

Now our widget can show and hide the content on demand. What we then need to do is to detect when particular
widget becomes visible/hidden and trigger the corresponding action.

Reacting to visibility state change

To detect when a particular widget becomes visible we’ll use
IntersectionObserver.
Intersection observer works with DOM elements, so we’ll need to get a hold of the DOM element that describes the content.
We already inject it into the directive:

@Directive({...})
export class ContainerDirective {
  constructor(
    public element: ElementRef,
    public viewContainerRef: ViewContainerRef
  ) {}
}

But we also need to somehow pass to the parent component that can register it with the observer.
For that we could use DI and simple registry pattern. Here’s a bit abridged implementation for that:

abstract class Registry {
  abstract registerContainer(cmp): void;
  abstract removeContainer(cmp): void;
}

@Component({
  selector: 'd-rv',
  providers: [
    { provide: Registry, useExisting: DRvComponent }
  ]
})
export class DRvComponent implements Registry {
  items = [...];

  registerContainer(cmp) {
    const item = this.items.find((c) => c === cmp.item);
    item.container = cmp;
  }

  removeContainer(cmp) {}
}

@Directive({...})
export class ContainerDirective {
  constructor( private registry: Registry) {}

  ngOnInit() {
    this.item = this.params.item;
    this.embeddedViewRef = this.params.tpl.createEmbeddedView({ item: this.params.item });
    this.viewContainerRef.insert(this.embeddedViewRef);

    this.registry.registerContainer(this);
  }

  show() {}
  hide() {}

  ngOnDestroy() {
    this.registry.removeContainer(this);
  }
}

The last thing that remains to do is:

  • define an observer and register DOM elements with it when then are added to registry
  • implement logic that reacts to visibility changes and calls the show or hide method on the corresponding widget

Here’s the implementation for that:

@Component({...})
export class DRvComponent implements Registry {
  private readonly intersectionObserver = new IntersectionObserver(
    (entries) => this.intersectionObserverCallback(entries),
  );

  private intersectionObserverCallback(entries): void {
    const updateQueue = new Map();

    entries.forEach((entry) => {
      if (!entry.target.isConnected) return;

      const transitionToVisible = entry.isIntersecting;
      updateQueue.set(entry.target, transitionToVisible);
    });

    requestAnimationFrame(() => this.updateWidgetVisibility(updateQueue));
  }

  updateWidgetVisibility(updateQueue): void {
    updateQueue.forEach((visible, el) => {
      const { container } = this.items.find(
        (i) => i.container.element.nativeElement === el
      );
      if (visible) {
        container.show();
      } else {
        container.hide();
      }
    });
  }
}

Now we can put it all together:

abstract class Registry {
	abstract registerContainer(cmp): void;
	abstract removeContainer(cmp): void;
}

@Component({
	selector: "d-rv",
	template: `
		<div *ngFor="let item of items" class="wrapper">
			<div [container]="{item, tpl}"></div>
		</div>
		<ng-template #tpl let-item="item">
			<span class="container-content">{{ item.name }}</span>
		</ng-template>
	`,
	styles: [
		`
			.wrapper {
				background-color: #e6e6e6;
				display: flex;
				flex-direction: column;
				justify-content: center;
				align-items: center;
				margin: 20px 0;
				border: 1px solid #d5d5f1;
				min-height: 150px;
				min-width: 200px;
			}

			.container-content {
				margin: 10px;
				padding: 40px;
				background: lightseagreen;
			}
		`,
	],
	providers: [{ provide: Registry, useExisting: DRvComponent }],
})
export class DRvComponent implements Registry {
	items = [
		{ name: "Home 1", container: null },
		{ name: "Home 2", container: null },
		{ name: "Home 3", container: null },
		{ name: "Home 4", container: null },
	];

	registerContainer(cmp) {
		const item = this.items.find((c) => c === cmp.item);
		item.container = cmp;
		this.intersectionObserver.observe(cmp.element.nativeElement);
	}

	removeContainer(cmp) {
		this.intersectionObserver.unobserve(cmp.element.nativeElement);
	}

	private readonly intersectionObserver = new IntersectionObserver((entries) =>
		this.intersectionObserverCallback(entries)
	);

	private intersectionObserverCallback(entries): void {
		const updateQueue = new Map();

		entries.forEach((entry) => {
			if (!entry.target.isConnected) return;

			const transitionToVisible = entry.isIntersecting;
			updateQueue.set(entry.target, transitionToVisible);
		});

		requestAnimationFrame(() => this.updateWidgetVisibility(updateQueue));
	}

	updateWidgetVisibility(updateQueue): void {
		updateQueue.forEach((visible, el) => {
			const { container } = this.items.find(
				({ container }) => container.element.nativeElement === el
			);
			if (visible) {
				container.show();
			} else {
				container.hide();
			}
		});
	}
}

@Directive({
	selector: "[container]",
})
export class ContainerDirective {
	@Input("container") params;
	embeddedViewRef;
	item;

	constructor(
		public element: ElementRef,
		public viewContainerRef: ViewContainerRef,
		private registry: Registry
	) {}

	ngOnInit() {
		this.item = this.params.item;
		this.embeddedViewRef = this.params.tpl.createEmbeddedView({
			item: this.params.item,
		});
		this.viewContainerRef.insert(this.embeddedViewRef);

		this.registry.registerContainer(this);
	}

	show(): void {
		if (this.embeddedViewRef) {
			this.viewContainerRef.insert(this.embeddedViewRef);
		}
	}

	hide(): void {
		this.viewContainerRef.detach();
	}

	ngOnDestroy() {
		this.registry.removeContainer(this);
	}
}

When we run the example, we can see our optimization in action:

As widgets become visible, we render their internal content. This increases the number of DOM elements.
On hide, we remove the content and the number of DOM elements decreases.

To make sure that views are not re-created, we could put a logpoint inside the creatLView
function and check how many times it’s executed:

Image alt

We could also add logpoints to show method that inserts the view:

Image alt

So now when we run the setup, here’s what we’ll see:

Image alt

As you can see it created views eagerly for all widgets,
but when a widget later becomes hidden and visible the view is no longer created:

We could even optimize the code and not create the view until it’s needed:

@Directive({...})
export class ContainerDirective {
  @Input('container') params;
  embeddedViewRef;
  item;

  constructor(
    public element: ElementRef,
    public viewContainerRef: ViewContainerRef,
    private registry: Registry
  ) {}

  ngOnInit() {
    this.item = this.params.item;
    this.registry.registerContainer(this);
  }

  show(): void {
    if (this.embeddedViewRef) {
      this.viewContainerRef.insert(this.embeddedViewRef);
    } else {
      this.embeddedViewRef = this.params.tpl.createEmbeddedView({ item: this.params.item });
      this.viewContainerRef.insert(this.embeddedViewRef);
    }
  }
}

and here’s the how it looks:

Share this post

Sign up for our newsletter

Stay up-to-date with the trends and be a part of a thriving community.