Lazy loading offscreen images for Angular ngFor list
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.
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
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
ng-lazy-load demo - using lazyLoad directive in async ngFor loop with animation, ng-defer-load issue - images are…stackblitz.com
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. Original
deferLoad 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.
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
LazyLoadServiceto defer the API registration timing
[url]input to bypass the registration for some performance gain
[index]input to control the load timing
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
Thanks for reading.