Published on

Angular Ivy Engine: Efficient Rendering Without Virtual DOM Overhead

Introduction

Introduced in Angular 9, Ivy became the default rendering engine, replacing the older View Engine to bring enhanced efficiency and performance. Angular 18 builds upon Ivy's robust foundation, adding new optimizations and advanced features that continue to leverage its capabilities.

One of Ivy's core principles is its Incremental DOM approach. Unlike React.js, which re-renders the virtual DOM/ Fiber tree and then calculates differences (or "reconciling") before applying changes, Angular Ivy updates only the parts of the DOM that have actually changed. This targeted approach minimizes memory usage because Ivy doesn't require additional memory to re-render components unless the DOM is actually modified. Memory is allocated only when new DOM nodes are added or removed, and the size of the allocation corresponds directly to the scope of the change.

It’s not that React is inherently slow — React is, in fact, very performant, especially with techniques like React.memo, useMemo hooks, and optimal state management to avoid unnecessary re-renders, moving state down or using children props. However, Angular's Ivy engine delivers similar efficiencies by default. Additionally, Angular offers the OnPush change detection strategy, which, much like React.memo, enables more granular control over re-renders and can further boost application performance.

Compilation Phase: Rendering Instructions

At build time (or JIT if you're not using AOT), Angular uses the component’s decorator metadata (e.g., @Component) to generate a component definition. This process translates HTML templates into low-level rendering instructions, stored within the component definition. These instructions describe the DOM structure, binding information, and event listeners, laying the groundwork for optimized updates.

Consider this example component in Angular 18.2.0:

@Component({
  selector: 'app-root',
  template: `
    <app-counter [items]="items" [counter]="counter"></app-counter>
    <button (click)="addItem()">Add Item</button>
    <button (click)="increaseCounter()">Increment</button>
  `
})
export class AppComponent {
  items = ['item1', 'item2', 'item3'];
  counter = 0;

  addItem() {
    this.items.push(`item${this.items.length + 1}`);
  }

  increaseCounter() {
    this.counter++;
  }
}

@Component({
  selector: 'app-counter',
  template: `
    <h1>A Counter: {{ counter }}</h1>
    <ul>
      <li *ngFor="let item of items">{{ item }}</li>
    </ul>
  `
})
export class CounterComponent {
  @Input() counter!: number;
  @Input() items!: string[];
}

The component will compile into Angular Ivy's optimized instruction set (e.g., ɵɵelementStart, ɵɵtext, ɵɵproperty). It's worth noting that ɵ (Theta) is used to signify methods that are private to Angular's framework internals and should not be accessed directly by developers.

angular-ivy-compiled-component-class-screenshots

Changing Detection: Detecting Asynchronous Operations

Angular leverages Zone.js to detect asynchronous operations. Zone.js patches asynchronous APIs like setTimeout, Promise, and event listeners to notify Angular when these operations complete. This allows Angular to initiate change detection at the right time, ensuring that the DOM is updated only when necessary.

When app initializes, Angular subscribes to zone.onMicrotaskEmpty, an observable that emits whenever asynchronous tasks complete and the microtask queue empties. Once onMicrotaskEmpty emits, Angular invokes applicationRef.tick(), initiating the change detection process. The tick() in turn will execute the component definition's template function, which will update the DOM based on the instructions generated during the compilation phase.

angular-ivy-updating-applicationRef.tick()-screenshot

Execution Phase: Updating the DOM

Skipping Unchanged Instructions

Imagine clicking the "Add Item" button in our example. This action updates the <ul> list of items but leaves the <h1> counter unchanged. angular-ivy-updating-template-function-invoked-screenshot
Angular Ivy’s incremental DOM approach will update only the <ul> list, bypassing any re-render of <h1>. This means that any instruction associated with the <h1> counter is skipped, conserving memory and CPU cycles. angular-ivy-updating-skip-text-updating-screenshot

Executing the Instructions

If the "Increment" button is clicked, however, Angular will execute the instruction for <h1>, triggering the updateTextNode() method to update the counter's displayed value.

angular-ivy-updating-updateTextNode-screenshot
If you click parentElement in the left pane, you can observe the DOM element in the DevTools window, so the updateTextNode() method does refer the real DOM element. angular-ivy-updating-DevTools-screenshot

Conclusion

Each component template in Angular is compiled into a sequence of instructions (such as ɵɵelementStart, ɵɵtext, and ɵɵproperty) that are embedded directly in the component's definition. Unused components are tree-shaken out of the final bundle, reducing the bundle size for faster loads.

Angular Ivy only invokes instructions related to the specific changes in state or properties, skipping over unaffected elements. This incremental DOM approach negates the need for extra memory for re-rendering, making Ivy an exceptionally memory-efficient rendering engine.