Lazy loading images in Loklak Search

Loklak Search delivers the media rich content to the users. Most of the media delivered to the users are in the form of images. In the earlier versions of loklak search, these images were delivered to the users imperatively, irrespective of their need. What this meant is, whether the image is required by the user or not it was delivered, consuming the bandwidth and slowing down the initial load of the app as large amount of data was required to be fetched before the page was ready. Also, the 404 errors were also not being handled, thus giving the feel of a broken UI.

So we required a mechanism to control this loading process and tap into its various aspects, to handle the edge cases. This, on the whole, required few new Web Standard APIs to enable smooth working of this feature. These API’s are

  • IntersectionObserver API
  • Fetch API

 

As the details of this feature are involving and comprise of new API standards, I have divided this into two posts, one with the basics of the above mentioned API’s and the outline approach to the module and its subcomponents and the difficulties which we faced. The second post will mostly comprise of the details of the code which went into making this feature and how we tackled the corner cases in the path.

Overview

Our goal here was to create a component which can lazily load the images and provide UI feedback to the user in case of any load or parse error. As mentioned above the development of this feature depends on the relatively new web standards API’s so it’s important to understand the functioning of these AP’s we understand how they become the intrinsic part of our LazyImgComponent.

Intersection Observer

If we see history, the problem of intersection of two elements on the web in a performant way has been impossible since always, because it requires DOM polling constantly for the ClientRect the element which we want to check for intersection, as these operations run on main thread these kinds of polling has always been a source of bottlenecks in the application performance.

The intersection observer API is a web standard to detect when two DOM elements intersect with each other. The intersection observer API helps us to configure a callback whenever an element called target intersects with another element (root) or viewport.

To create an intersection observer is a simple task we just have to create a new instance of the observer.

var observer = new IntersectionObserver(callback, options);

Here the callback is the function to run whenever some observed element intersect with the element supplied to the observer. This element is configured in the options object passed to the Intersection Observer

var options = {
root: document.querySelector('#root'), // Defaults to viewport if null
rootMargin: '0px', // The margin around root within which the callback is triggered
threshold: 1.0
}

The target element whose intersection is to be tested with the main element can be setup using the observe method of the observer.

var target = document.querySelector('#target');
observer.observe(target);

After this setup whenever the target element intersects with the root element the callback method is fired, and this provides the easy standard mechanism to get callbacks whenever the target element intersects with root element.

How this is used in Loklak Search?

Our goal here is to load the images only if required, ie. we should load the images lazily only if they are in the viewport. So the task of checking whether the element is near the viewport is done in a performant way using the Intersection Observer standard.

Fetch API

Fetch API provides interface for fetching resources. It provides us with generic Request and Response interfaces, which can be used as Streaming responses, requests from Service Worker or CacheAPI. This is a very lightweight API providing us with the flexibility and power to make the AJAX requests from anywhere, irrespective of context of the thread or the worker. It is also a Promise driven API. Thus, providing the remedy from the callback hell.

The basic fetch requests are really very easy to setup, all they require is the path to the resource to fetch, and they return a promise on which we can apply promise chaining to transform the response into desired form. A very simple example illustratuing the fetch API is as follows.

fetch('someResource.xyz’')
.then(function(response) {
return response.blob();
})
.then(function(respBlob) {
doSomethingWithBlob(respBlob);
});

The role of fetch api is pretty straight forward in the lazy loading of images, ie. to actually fetch the images when requested.

Lazy Image Component

We start designing our lazy image component by structuring our API. The API our component should provide to the outside world must be similar to Native Image Element, in terms of attributes and methods. So our component’s class should look something like this, with attributes src, alt, title, width and height. Also, we have hooked a load event which host can listen to for loading success or failure of the image.

@Component({
selector: 'app-lazy-img',
templateUrl: './lazy-img.component.html',
styleUrls: ['./lazy-img.component.scss'],
})
export class LazyImgComponent {
@Input() src: string;
@Input() width: number;
@Input() height: number;
@Input() alt: string;
@Input() title: string;
@Output() load: EventEmitter<boolean> = new EventEmitter<boolean>();
}

This basic API provides us with our custom <app-lazy-img> tag which we can use with the attributes, instead of standard <img> element to load the images when needed.

So our component is basically a wrapper around the standard img element to provide lazy loading behaviour.

This is the basic outline of how our component will be working.

  • The component registers with an Intersection observer, which notifies the element when it comes near the viewport.
  • Upon this notification, the component fetches the resource image.
  • When the fetch is complete, the retrieved binary stream is fed to the native image element which eventually renders the image on the screen.

This is the basic setup and working logic behind our lazy image component. In next post, I will be discussing the details of how this logic is achieved inside the LazyImgComponent, and how we solve the problems with a large number of elements rendered at once in the application.

Resources and Links

  • Intersection observer API
  • Intersection Observer polyfill for the browsers which don’t support Intersection Observer
  • Fetch API documentation
  • Fetch API polyfill for the browsers which don’t support fetch.
  • Loklak Search Repo