Lazy loading images is an effective way to improve the performance of your Angular applications, especially when dealing with large images or pages with many media assets. In this blog post, we'll walk through how to create an Angular directive that lazy loads images using the IntersectionObserver API.
What is Lazy Loading?
Lazy loading is a performance optimization technique where resources (like images) are only loaded when they are about to be displayed in the viewport. This reduces the initial page load time and saves bandwidth by loading images only when necessary.
Introducing the IntersectionObserver API
The IntersectionObserver API is a modern browser feature that allows developers to asynchronously detect when an element enters or leaves the viewport. Unlike older techniques such as scroll events, IntersectionObserver works independently of the main JavaScript execution thread, allowing smoother interactions and better performance.
The API is designed for tasks like lazy loading, infinite scrolling, or implementing "scroll-triggered" animations without the need to constantly monitor scroll events, which can lead to performance issues. With IntersectionObserver, the browser efficiently manages the observation of DOM elements in a way that doesn’t interrupt the main thread or user interactions.
Why Use IntersectionObserver?
There are several benefits to using IntersectionObserver over traditional methods:
- Non-blocking behavior: It works asynchronously and doesn’t block the main thread. This is a huge improvement over older techniques like scroll event listeners, which can slow down rendering when tracking multiple elements.
- Reduced CPU overhead: Instead of manually tracking the scroll position or using
setTimeout/setInterval, the browser natively handles the monitoring of elements. This reduces the need for expensive layout recalculations. - Efficient resource management: You can observe multiple elements at once with a single observer, instead of manually attaching event listeners to each image.
- Better user experience: Lazy loading reduces the initial page load time, allowing users to access visible content faster without waiting for all media resources to download.
Creating the Lazy Loading Directive
Now let's create an Angular directive that lazy loads images using IntersectionObserver. The directive will monitor when an image enters the viewport and load it dynamically. Here’s the implementation:
import {
AfterViewInit,
Directive,
ElementRef,
inject,
Inject,
Input, OnDestroy,
PLATFORM_ID,
Renderer2
} from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
@Directive({
standalone: true,
selector: '[saiLazyImgLoad]'
})
export class ImgLazyLoadDirective implements AfterViewInit, OnDestroy {
readonly elementRef: ElementRef = inject(ElementRef);
readonly renderer2: Renderer2 = inject(Renderer2);
private obs!: IntersectionObserver;
@Input() imgSrc?: string;
@Input() backGroundImageClass?: string;
constructor(
@Inject(PLATFORM_ID) private platform: NonNullable
) {}
ngAfterViewInit(): void {
if (isPlatformBrowser(this.platform)) {
this.hasIntersectionObserver()
? this.observerIntersection()
: this.loadImage();
}
}
private hasIntersectionObserver() {
return window && 'IntersectionObserver' in window;
}
private observerIntersection(): void {
this.obs = new IntersectionObserver((entries) => {
entries.forEach(({ isIntersecting }) => {
if (isIntersecting) {
this.loadImage();
this.obs.disconnect();
}
});
}, { threshold: 0.1 });
this.obs.observe(this.elementRef.nativeElement);
}
private loadImage(): void {
if (this.backGroundImageClass) {
this.renderer2.addClass(this.elementRef.nativeElement, this.backGroundImageClass);
return;
}
this.elementRef.nativeElement.src = this.imgSrc;
}
ngOnDestroy(): void {
this.obs?.disconnect();
}
}
How It Works
The ImgLazyLoadDirective works as follows:
- It first checks if the application is running in the browser (since lazy loading won’t work on the server side).
- It verifies whether the browser supports
IntersectionObserver. If supported, it sets up an observer to track when the image is about to enter the viewport. - When the image comes into view, the directive dynamically loads the image (or background image) and disconnects the observer to save resources.
- If the browser doesn't support
IntersectionObserver, the image is loaded immediately.
Advantages of IntersectionObserver over Other Methods
Previously, developers relied on techniques such as listening to scroll events to determine when an image should be loaded. This approach required constant checks and recalculations as the user scrolled, which could degrade performance, especially with a large number of images or elements on the page.
The IntersectionObserver API is a significant improvement because it:
- Doesn’t block the main thread: Unlike scroll events, which require recalculations during scrolling,
IntersectionObserverruns asynchronously. It efficiently monitors changes to element visibility in a way that doesn't interfere with user interactions or UI rendering. - Optimizes performance: Browsers internally manage visibility checks, so the process is highly optimized, and multiple elements can be observed with minimal overhead.
- Native browser support: The API is supported natively in modern browsers, eliminating the need for complex third-party libraries or polyfills for basic lazy loading functionality.
Using the Directive
You can use the directive in your Angular templates by simply applying the saiLazyImgLoad attribute to an image element and providing the imgSrc or background image class.
Example with Image Element
<img [saiLazyImgLoad] [imgSrc]="imageUrl" alt="Lazy Loaded Image" />
Example with Background Image
<div [saiLazyImgLoad] [backGroundImageClass]="'lazy-loaded-bg'">
Content here
</div>
Conclusion
Using IntersectionObserver with Angular directives allows you to easily optimize image loading by only loading resources when they are visible to the user. This not only enhances the performance of your Angular applications but also provides a better user experience by reducing load times and saving bandwidth.
Happy coding!
