Web UI Magic: Reactivity Fundamentals and Angular Best Practices

7 minute read Modified:

Dive into JavaScript's event loop and web framework reactivity models, plus expert tips to supercharge your Angular apps.
Table of Contents

Web development has become more sophisticated, but understanding the fundamentals and different approaches to reactivity can help you write better, more efficient applications. This post will explain how web engines work, explore various reactivity models in different frameworks, and provide performance tips specifically for Angular developers.

1. How Web Engines Work ELI5: The DOM and the JS Event Loop

The DOM

The Document Object Model (DOM) is essentially a tree structure representing your web page. Each HTML element is a node in this tree, and JavaScript allows us to manipulate these nodes to dynamically update the content, structure, and style of the web page.

Example of the document object modles render tree
Example of the document object modles render tree

The JavaScript Event Loop

The JavaScript event loop is the mechanism that allows JavaScript to perform non-blocking operations. JavaScript is single-threaded, meaning it can only do one thing at a time. The event loop manages this by placing tasks (like user events, HTTP requests, etc.) in a queue and processing them one by one.

Visualization of the js call stack and event loop
Visualization of the js call stack and event loop

Components of the Event Loop and How It Works

  1. Call Stack: The call stack is where JavaScript keeps track of function calls. When a function is invoked, it gets added to the top of the stack, and when it finishes, it gets removed from the stack. JavaScript starts by executing all the initial code in the call stack.
  2. Web APIs: These are browser-provided APIs like setTimeout, DOM events, fetch, etc. They handle tasks asynchronously. When an async operation like setTimeout is encountered, it is handed off to the Web APIs.
  3. Callback Queue: This is a queue of functions that are ready to be executed once the call stack is clear. When an asynchronous operation completes, its callback function is placed in this queue.
  4. Event Loop: The event loop constantly checks if the call stack is empty and if there are any tasks in the callback queue. If the stack is empty, it moves the first task from the queue to the stack, allowing it to be executed.

Understanding this flow is crucial for grasping how frameworks manage reactivity. I also highly recommend watching this talk which goes more into detail “What the heck is the event loop anyway?”

2. Simplifying Web Development: Frameworks and Their Different Approaches to Reactivity

Reactivity in web development refers to the automatic update of the UI when the application’s state changes. Web frameworks employ various reactivity models to manage UI updates efficiently. These approaches can be categorized along a spectrum from coarse-grained to fine-grained reactivity.

JS frameworks on the reactivity scale
JS frameworks on the reactivity scale

Coarse-Grained Reactivity

This approach executes significant portions of application code to determine DOM updates. It’s easier to implement but potentially less efficient.

  • React and Angular lean towards this approach, re-executing components and checking references on state changes, though their underlying mechanisms differ. (Angular’s approach will be detailed in the next section.)

Middle Ground

  • Vue balances coarse and fine-grained reactivity using a runtime-based system with Refs (similar to Signals). It re-runs components on state changes but allows for more granular updates through its provide/inject API.
  • Svelte also fits here, using a compiler-based approach for .svelte files and a separate store mechanism for external reactivity. Its compiler optimizes updates efficiently, often re-executing less code than coarser-grained frameworks.

Fine-Grained Reactivity

This approach updates only specific DOM nodes needing changes, without re-executing large code portions. It’s more efficient but can be more complex to implement.

  • Solid and Qwik represent this approach, directly tying reactive signals to DOM updates.

Key Concepts

Understanding the core mechanisms behind reactive frameworks helps clarify their design choices and trade-offs. Three fundamental concepts shape the landscape of modern web development:

Virtual DOM vs. Direct DOM Manipulation

React and Vue utilize a Virtual DOM approach, creating an in-memory representation of the UI for efficient comparison and selective updates. In contrast, Svelte and Solid employ direct DOM manipulation, compiling components to optimize DOM updates at runtime. Angular uses a hybrid approach with its Incremental DOM. While Virtual DOM provides a layer of abstraction that simplifies development, direct manipulation can offer performance benefits at the cost of increased complexity.

Virtual DOM diffing mechanism visualized
Virtual DOM diffing mechanism visualized

Reactivity Primitives

Frameworks employ various primitives to track and propagate state changes:

  • Values: Basic state representation, requiring comparison for updates.
  • Observables: Used in Angular and Svelte, enabling complex data flows.
  • Signals/Refs: Modern approaches in Vue, Qwik, and Solid allow fine-grained updates. Angular v17 and Svelte v5 also adopted signals, suggesting signals might become a native JS API (I discuss these developments here: Bet on the Web - Why the future of applications is web based).

Compilation vs. Runtime Reactivity

Svelte and Solid use compilation-based approaches, optimizing reactivity at build time and potentially reducing runtime overhead. React, Vue, and Angular primarily handle reactivity during execution, offering greater flexibility for dynamic content at the expense of some performance. Qwik combines both approaches, using partial hydration to balance build-time optimization and runtime adaptability. This distinction highlights the trade-off between compile-time optimization and runtime flexibility.

These concepts underpin the design philosophies of modern frameworks, influencing their approaches to performance, developer experience, and application architecture.

Conclusion

In web development, choosing the right framework and reactivity model is crucial. Coarse-grained reactivity is simpler to implement but may lead to inefficiencies, while fine-grained reactivity offers better performance but requires deeper understanding.

As Miško Hevery argues, a broken app is easier to fix because its issues are obvious and straightforward to address. In contrast, a slow app is harder to optimize as it involves multiple complex adjustments, which can sometimes lead to additional problems.

Source: Exploring Reactivity Across Various Frameworks

3. Bonus: Best Performance Practices in Angular’s Reactivity Model

Angular’s reactivity model is designed to provide efficient and effective ways to manage data updates and UI rendering. To make the most out of Angular’s capabilities, developers should adopt certain performance best practices. Here are some key strategies to optimize your Angular applications:

Zone.js

Zone.js patches browser apis to detect changes
Zone.js patches browser apis to detect changes and make the app reactive

Zone.js plays a crucial role in Angular’s reactivity model. It monkey patches Web APIs to detect asynchronous tasks and then triggers change detection throughout the application. This approach, while convenient, can be inefficient because it may cause unnecessary re-checking of component properties and rerendering of both parent and child components.

Zoneless Future

Angular is moving towards a zoneless architecture. The elimination of Zone.js means fewer memory overheads and less main thread usage. Additionally, it leads to smaller bundle sizes. Developers can prepare for this transition by adopting practices that ease the shift from Zone.js to a more efficient and fine-grained reactivity model.

Change Detection on Push

Implementation

Change Detection Strategy: OnPush is a performance optimization technique that can be implemented right away for immediate benefits. By using ChangeDetectionStrategy.OnPush, Angular will only check the component and its children when explicitly triggered by an input change or an Observable/Subject emit. This drastically reduces redundant change detection cycles.

@Component({
  selector: 'app-example',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `...`
})
export class ExampleComponent {
}

Future-Ready

Adopting OnPush now will make it easier to switch to Angular’s zoneless architecture in the future. Once Zone.js is removed, Angular is expected to be on par with frameworks like Svelte and Solid in terms of performance.

Signals

Signals represent Angular’s new fine-grained reactivity model. Unlike traditional change detection, signals provide a more precise way to update the DOM structure. This model allows for efficient, targeted updates, reducing unnecessary renders and significantly improving application performance. Angular signals guide

Track By

Using trackBy in ngFor is another essential practice for enhancing performance. This function helps Angular identify items in a list that have changed, allowing it to update only the necessary elements instead of re-rendering the entire list. This leads to faster, more efficient updates, particularly with large datasets.

@Component({
  selector: 'app-list',
  template: `
   <li *ngFor="let post of posts; trackBy:identify">{{post.title}}</li>
  `
})
export class ListComponent {
  post:[];

  identify(index, item) {
     return post.id;
  }
 
}

Control Flow: Defer Tag

Angular’s new template syntax introduces powerful features such as the @defer directive and an enhanced @for loop:

  • Enhanced @for loop: It includes a faster diffing algorithm and built-in support for trackBy, further optimizing list rendering.
  • @defer directive: This allows elements to be loaded and rendered only when needed, such as when they come into the viewport. This lazy-loading mechanism conserves resources and improves initial load times.
<div class="flex-container">
  @for (post of this.posts;track post.id) {
    @defer (on viewport){
        <app-card [post]="post" class="flex-item"/>
    } @placeholder (minimum 2000ms) {
        <div>Loading..</div>
    }
  }
 </div>

By incorporating these practices, your Angular applications will not only perform better but also be better prepared for future enhancements in the framework. Embracing these strategies ensures efficient change detection, targeted updates, and reduced resource consumption, positioning your application at the forefront of modern web development.