Lazy loading offscreen images for Angular ngFor list

Dogs dream by Samwise Lee

While ago, I wrote an article about using IntersectionObserver API where I have used an Angular service to decide which image to observe and it worked perfectly ok for Joanne’s website which was rather a simple use case. I had another use case that I want to lazy load offscreen images — a web app called Our notes allows to create a group and add / delete note with optional image. All notes and images are stored and served on Google’s firebase cloud platform. So my template has ngFor loop to show all notes in the selected group:

As expected, the page load got slower as the number of attached images grow. Assuming that the main bottleneck is at images loading, I spent sometime to try a separate service dedicated to observe intersecting item — the same approach I took in previous article but gave up soon. Main problem was that I had to maintain a list which is similar to the original one, noteService.items$ to remember which items have image, which item is currently being observed and which item to observe next when current one is intersecting. I concluded that as an item and its attached image can change dynamically by user action, it was impractical to have another list for lazy loading.

Improving ng-defer-load

Then I found this little library that provides a directive called deferLoad to load element lazily. Nice thing about this library is that as it is a directive, it has a direct access to its host element to observe and can depends on life-cycle hooks such as ngOnInit and ngOnDestroy to register the API and deregister the callback. On the contrary, in the service approach, the service and each item should communicate somehow so that the registration / degistration are happening properly in sync with dynamic changes in the list. Unfortunately though when I tried this directive on my list, I soon found that it has no effect at all, all images were loaded all at once. It turned out that the timing of API registration needed to be delayed a bit to workaround the issue. I guess the animation I had on each item may have caused the issue. You can see the issue at the Stackblitz demo by toggling deferLoad.

To delay the API registration, I decided to introduce a new LazyLoadService and let it announce an order to register to all directives at delayed timing.

You may have noticed the default value of delayMsec property is zero. So each directive is to see the delayMsec property of the service and if the value is zero, it will register promptly at creation. If the value is greater than zero, it will instead subscribe to the announcedOrder observable and defer the registration.

Then a parent component that hosts the list would ask the service to defer the registration by 1500 msec for example at its creation.

Now the issue has gone, images are nicely lazy loaded while the page scrolls down. One other thing I wanted to improve was that the directive registers the API unconditionally whereas in reality note image was optional so for notes with no image the directive doesn’t need to register in first place. So I added an optional input [url] to the directive so as to bypass the registration at creation if it has no url.

Last thing I wanted to change was about when to load image. OriginaldeferLoad directive declares it’s time to load when an item is intersecting but what if you want to load couple of images ahead of intersecting item so that user doesn’t see loading animation as much as possible?

I ended up adding another optional input [index] to the directive and let it announce its index when it is intersecting, at the same time each directive will subscribe to announcedIntersection observable to compare the announced intersecting index with its own index, if the difference is small enough it means it is close to be shown on the screen so it’s time to load.

LazyLoadService helps directives communicate each other by providing announceIntersection method to be called by intersecting item and announcedIntersection observable to be subscribed by all directives, it also has loadAheadCount property with default value of 2 which can be overwritten by parent component at creation to control how many images to load ahead of intersecting item.

Now my template is using a new lazyLoad directive and looks like below.

Summary

I showed here how I extended ng-defer-load to make it useful for wider applications by adding following options, it worked quite nicely for my application so I published it as another npm package called ng-lazy-load.

  • LazyLoadService to defer the API registration timing
  • optional [url] input to bypass the registration for some performance gain
  • optional [index] input to control the load timing

The ng-lazy-load library is developed as one of the child project of Our notes app and full source code can be found here.

Note: As of now IntersectionObserver API is not natively supported on some browsers such as iOS Chrome and Safari, so you need to polyfill by adding intersection-observer.js in your angular.json.

Thanks for reading.