Optimizing Angular Performance with Intersection Observer

Ever wanted to trigger animations only when elements appear in the viewport? Or maybe load images and content just before they’re visible? That’s where the Intersection Observer API shines — and Angular makes it super smooth to integrate.

In this post, I’ll walk you through how I implemented a directive-based solution in Angular to detect visibility changes — ideal for lazy loading, infinite scrolls, or scroll-triggered animations.


⚡ What’s the Intersection Observer API?

Instead of attaching heavy scroll or resize event listeners, Intersection Observer gives you a cleaner, more performant way to monitor when DOM elements enter or exit the viewport (or a scrollable container).

✅ Why Use It?

  • Better Performance: No janky scroll listeners.
  • Asynchronous + Efficient: Observers run in the background.
  • Configurable: Thresholds, margins, and custom containers supported.

🧱 My Angular Setup

Let’s dive into how I built a reusable Angular directive that lets me observe visibility changes for any component or element.


🧾 Step 1: HTML Template (app.component.html)

<div style="display: flex; overflow: scroll" #el>
<div
*ngFor="let item of items"
class="row"
[appIntersection]
(viewChange)="handleViewChange($event, item)"
>
{{ item }}
</div>
</div>

<div style="position: fixed; bottom: 5rem">
<span *ngFor="let it of itemsInView">{{ it }} - </span>
</div>

🎨 Step 2: Styling (app.component.css)

.row {
height: 500px;
min-width: 500px;
margin: 1rem;
padding: 1rem;
background-color: rebeccapurple;
color: white;
font-weight: bold;
display: flex;
align-items: center;
justify-content: center;
}

🧠 Step 3: Component Logic (app.component.ts)

import { Component } from '@angular/core';

@Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
})
export class AppComponent {
items = Array.from({ length: 10 }, (_, i) => `Row ${i + 1}`);
itemsInView = new Set<string>();

handleViewChange(event: { isIntersecting: boolean }, item: string) {
if (event.isIntersecting) {
this.itemsInView.add(item);
} else {
this.itemsInView.delete(item);
}
}
}

🧩 Step 4: Custom Directive (intersection.directive.ts)

import {
Directive,
ElementRef,
Output,
EventEmitter,
Input,
AfterViewInit,
OnDestroy
} from '@angular/core';

@Directive({
selector: '[appIntersection]',
})
export class IntersectionDirective implements AfterViewInit, OnDestroy {
@Input() appIntersection: IntersectionObserverInit = {};
@Output() viewChange = new EventEmitter();
private observer!: IntersectionObserver;

constructor(private el: ElementRef) {}

ngAfterViewInit() {
this.observer = new IntersectionObserver((entries) => {
const entry = entries[0];
this.viewChange.emit({
isIntersecting: entry.isIntersecting,
intersectionRatio: entry.intersectionRatio,
});
}, this.appIntersection);

this.observer.observe(this.el.nativeElement);
}

ngOnDestroy() {
this.observer.unobserve(this.el.nativeElement);
this.observer.disconnect();
}
}

⚙️ How This Works

  1. The directive hooks into the DOM element.
  2. The IntersectionObserver watches for visibility changes.
  3. On intersection, we emit a custom event (viewChange).
  4. The component listens and updates a set of “visible” items in real time.

🔄 Use Cases You’ll Love

  • 📷 Lazy Load Images: Only load what the user is about to see.
  • 📜 Infinite Scroll: Fetch more data as needed.
  • ✨ Animate On Scroll: Trigger CSS or JS animations when content enters view.
  • 🧪 A/B Testing UX: Monitor which parts of your UI users actually reach.

✅ Final Thoughts

Integrating the Intersection Observer API in Angular is super clean when paired with a directive-based approach. You get performance gains, a modular structure, and total control over how elements react to visibility — without the headaches of scroll tracking.

Start small, modularize the logic, and you’ll find dozens of clever ways to use this.

Leave a Comment

Your email address will not be published. Required fields are marked *