DEV Community

Cover image for Supercharge Angular: Advanced Techniques for Lightning-Fast Rendering and Optimization
Aarav Joshi
Aarav Joshi

Posted on

Supercharge Angular: Advanced Techniques for Lightning-Fast Rendering and Optimization

Angular's Incremental DOM is a game-changer for rendering optimization. It's all about smart updates and minimal DOM operations. Let's dig into some advanced techniques to supercharge your Angular apps.

First up, change detection strategies. Angular's default change detection is pretty smart, but we can make it even smarter. By using OnPush, we tell Angular to only check for changes when inputs change or events occur. This can drastically reduce the number of checks in complex apps.

Here's how to implement OnPush:

@Component({
  selector: 'my-component',
  template: '...',
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class MyComponent { }
Enter fullscreen mode Exit fullscreen mode

But we can go further. Manual dirty checking gives us fine-grained control over when change detection runs. We can mark components as dirty only when we know they've changed:

export class MyComponent {
  constructor(private cd: ChangeDetectorRef) {}

  updateData() {
    // Update component data
    this.cd.markForCheck();
  }
}
Enter fullscreen mode Exit fullscreen mode

Now, let's talk about ngFor optimization. The trackBy function is a powerful tool for improving list rendering performance. Instead of re-rendering the entire list when data changes, Angular can track individual items and only update what's necessary.

@Component({
  template: `
    <li *ngFor="let item of items; trackBy: trackByFn">
      {{item.name}}
    </li>
  `
})
export class MyComponent {
  trackByFn(index, item) {
    return item.id; // Unique identifier
  }
}
Enter fullscreen mode Exit fullscreen mode

Custom structural directives can work wonders with Incremental DOM. They allow us to create reusable rendering logic that's optimized for performance. Here's a simple example of a custom *ifLoaded directive:

@Directive({
  selector: '[ifLoaded]'
})
export class IfLoadedDirective {
  constructor(
    private templateRef: TemplateRef<any>,
    private viewContainer: ViewContainerRef
  ) {}

  @Input() set ifLoaded(condition: boolean) {
    if (condition) {
      this.viewContainer.createEmbeddedView(this.templateRef);
    } else {
      this.viewContainer.clear();
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

This directive only renders its content when the condition is true, saving unnecessary DOM operations.

Dynamic view encapsulation is another powerful technique. By changing the encapsulation mode at runtime, we can optimize rendering for different scenarios. For example, we might use ShadowDom for isolated components and None for global styles:

@Component({
  selector: 'my-component',
  template: '...',
  encapsulation: ViewEncapsulation.ShadowDom
})
export class MyComponent {
  changeEncapsulation() {
    this.encapsulation = ViewEncapsulation.None;
  }
}
Enter fullscreen mode Exit fullscreen mode

Let's dive into custom rendering pipelines. Angular's renderer is extensible, allowing us to create optimized rendering strategies for specific use cases. For instance, we could create a renderer that batches DOM updates for smoother animations:

class BatchingRenderer extends Renderer2 {
  private updateQueue = [];

  createElement(name: string, namespace?: string) {
    const el = document.createElement(name);
    this.updateQueue.push(() => document.body.appendChild(el));
    return el;
  }

  // Implement other Renderer2 methods...

  flushUpdates() {
    this.updateQueue.forEach(update => update());
    this.updateQueue = [];
  }
}
Enter fullscreen mode Exit fullscreen mode

This custom renderer queues up DOM operations and applies them in batches, reducing layout thrashing.

Now, let's talk about detaching change detectors. This technique can significantly boost performance in scenarios where parts of your app don't need constant updating. Here's how you might use it:

export class HeavyComponent implements OnInit, OnDestroy {
  constructor(private cd: ChangeDetectorRef) {}

  ngOnInit() {
    this.cd.detach(); // Detach from change detection
  }

  ngOnDestroy() {
    this.cd.reattach(); // Don't forget to reattach!
  }

  manualCheck() {
    this.cd.detectChanges(); // Run change detection manually
  }
}
Enter fullscreen mode Exit fullscreen mode

By detaching the change detector, we prevent Angular from checking this component on every cycle. We can then manually trigger change detection when needed.

Template expression optimization is crucial for Incremental DOM performance. Complex expressions in templates can slow down rendering. Let's look at some best practices:

  1. Avoid function calls in templates. They're executed on every change detection cycle.
  2. Use pure pipes for transformations instead of methods.
  3. Memoize expensive computations.

Here's an example of using a pure pipe instead of a method:

@Pipe({
  name: 'expensive',
  pure: true
})
export class ExpensivePipe implements PipeTransform {
  transform(value: any): any {
    // Expensive computation here
    return result;
  }
}

// In template:
// {{ value | expensive }} instead of {{ expensiveMethod(value) }}
Enter fullscreen mode Exit fullscreen mode

Let's explore zone.js and how we can leverage it for performance. Zone.js is the magic behind Angular's change detection, but we can also use it to our advantage. We can run code outside of Angular's zone to avoid triggering change detection:

export class MyComponent {
  constructor(private zone: NgZone) {}

  heavyComputation() {
    this.zone.runOutsideAngular(() => {
      // Perform heavy computation here
      // Angular won't trigger change detection for this code
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

This is particularly useful for operations that don't affect the view, like complex calculations or non-UI related background tasks.

Now, let's delve into the world of WebWorkers and how they can enhance Angular's rendering performance. WebWorkers allow us to offload heavy computations to a separate thread, keeping our main thread free for smooth UI updates. Here's a basic example:

// In your component
const worker = new Worker('./app.worker', { type: 'module' });
worker.onmessage = ({ data }) => {
  console.log(`Result: ${data}`);
};
worker.postMessage(42);

// In app.worker.ts
addEventListener('message', ({ data }) => {
  const result = heavyComputation(data);
  postMessage(result);
});
Enter fullscreen mode Exit fullscreen mode

This approach can significantly improve perceived performance, especially for data-intensive applications.

Let's talk about lazy loading and how it ties into Incremental DOM. Lazy loading not only improves initial load time but also helps with rendering performance by reducing the amount of code that needs to be processed. Here's how you might set up a lazy-loaded route:

const routes: Routes = [
  {
    path: 'lazy',
    loadChildren: () => import('./lazy/lazy.module').then(m => m.LazyModule)
  }
];
Enter fullscreen mode Exit fullscreen mode

This technique works hand in hand with Incremental DOM to ensure that only the necessary components are rendered and updated.

Another advanced technique is using Observables for fine-grained updates. By leveraging RxJS, we can create streams of data that trigger updates only when necessary. Here's an example:

@Component({
  template: '{{ data$ | async }}'
})
export class MyComponent {
  data$ = new BehaviorSubject('Initial value');

  updateData() {
    this.data$.next('New value');
  }
}
Enter fullscreen mode Exit fullscreen mode

This approach allows for more efficient updates, as Angular only needs to check the latest emitted value.

Let's explore the concept of "smart" and "dumb" components in the context of Incremental DOM. Smart components handle business logic and state, while dumb components focus solely on presentation. This separation can lead to more efficient rendering:

// Smart component
@Component({
  selector: 'smart-component',
  template: '<dumb-component [data]="data"></dumb-component>'
})
export class SmartComponent {
  data = 'Some data';
}

// Dumb component
@Component({
  selector: 'dumb-component',
  template: '{{ data }}',
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class DumbComponent {
  @Input() data: string;
}
Enter fullscreen mode Exit fullscreen mode

The dumb component, using OnPush, will only update when its input changes, reducing unnecessary checks.

Virtual scrolling is another technique that works well with Incremental DOM. It allows us to render only the visible items in a long list, greatly improving performance for large datasets. Angular provides the CDK Virtual Scroll module for this:

<cdk-virtual-scroll-viewport itemSize="50" class="example-viewport">
  <div *cdkVirtualFor="let item of items" class="example-item">{{item}}</div>
</cdk-virtual-scroll-viewport>
Enter fullscreen mode Exit fullscreen mode

This approach ensures that only the visible items are rendered and updated, regardless of how large the dataset is.

Let's discuss the importance of AOT (Ahead-of-Time) compilation in the context of Incremental DOM. AOT compilation converts your Angular templates into highly optimized JavaScript code at build time, which can significantly improve runtime performance. It works particularly well with Incremental DOM, as it can generate more efficient update instructions.

Memory management is crucial for maintaining good rendering performance over time. Angular's Incremental DOM is generally good at managing memory, but we can help by ensuring we're not holding onto unnecessary references. Always unsubscribe from observables and clear any timers in the ngOnDestroy lifecycle hook:

export class MyComponent implements OnDestroy {
  private subscription: Subscription;

  ngOnInit() {
    this.subscription = someObservable.subscribe(/* ... */);
  }

  ngOnDestroy() {
    if (this.subscription) {
      this.subscription.unsubscribe();
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Finally, let's touch on the importance of profiling and measuring performance. The Angular DevTools extension provides valuable insights into your app's rendering performance. Use it to identify bottlenecks and verify that your optimizations are having the desired effect.

Remember, optimization is an iterative process. Start with the low-hanging fruit, measure the impact, and then move on to more advanced techniques as needed. With these tools and techniques in your arsenal, you're well-equipped to build blazing-fast Angular applications that can handle even the most demanding rendering scenarios.


Our Creations

Be sure to check out our creations:

Investor Central | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | JS Schools


We are on Medium

Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva

Top comments (0)