React — Lazy Load images with Cloudinary and Intersection Observers

Lazy loading is one of those topics which never gets old, I first saw them back in jQuery days. Anyway as javascript advances and new paradigms and frameworks are introduced we must of course update our new way of doing old things.

So a while back ago I was looking for a solution to implement lazy loading images in React. First thing I came across was the react-lazyload package. It is good, but it’s always better if you first implement your own version, at least for small and quick tasks like this, because in my opinion the amount of new things you learn by doing it yourself is more valuable than the time you save by just npm-installing it.

One more thing before starting to code; the use of Cloudinary here is of course not necessary to follow along. I use it because it lets you apply filters (like blur, which we’ll be using) and query for different sizes of images on the fly. No more talk!

So the key idea here is : Have a placeholder <img /> tag which always is hidden, as its src , it has the link to the actual (high quality) image, when it’s loaded it invokes a function to say that it’s ready, at that moment the isLoaded property from the state is updated and the container <div> will change its backgroung-image from the blurred low-quality ( height : 10px in my case ~ 1KB) to the high-quality loaded image (line 52).

But it was the idea before adding the Intersection Observers though, doing like this we will be loading all the images in the page when the component is mounted. Intersection Observer comes into play to let us load images as they start to enter the viewport, let me show you how: when the component is mounted we create a new observer for our <div>. the Intersection Observer constructor takes two arguments, first the callback to call when the intersection percentage between the target 🎯element and its ancestor is greater than or equal to our threshold (the target can have any shape, but for calculating the intersection, the smallest rectangular that includes it will be considered). and second, options (line 15). root is the ancestor you want to compare this target with, null means just use the browser’s viewport. rootMargin is simply the margin you want around your root, using rootMargin you can enlarge or shrink your viewport in a sense, for example setting rootMargin : "100% 0%" we will ask the observer to start calculating the intersection when the target is100% of height of the viewport from the top and bottom edges of the viewport. And threshold can be either a single value or an array of values (think of them as percentages) at which, the callback will be called. So if you want to get notified when your component intersection with the viewport is at 25%, 50% and 75%, you can simply setthreshold : [0.25, 0.5, 0.75] . Then on line 22 we start observing our componentobserver.observe(target).

After we have set up the observer for our little component, on line 25 we define our callback function for it. The callback takes two arguments, entries (all the observers elements that have reached their threshold), and second, the observer object itself. So when the component is mounted we start observing if there are any elements that satisfy our conditions ( line 27, one important point here is that it is not guaranteed that the value you receive in intersectionRatio is exactly your threshold value, you must think of threshold as a minimum value). If that is the case, in our intersectionObserverCallback we loop over them and update the state, setting isLoaded to true. This will remove the <img /> tag and background of our <div> has will change, then since the image is loaded we can safely remove the observer and we’ll do that with observer.unobserve(target) .

In the render function, I’m checking to see if the component is visible enough to start loading the high quality image or not, when it is, the <img /> tag will be added and when done, it will be removed. In album-cover-container I just added a simple animation like transition : all 0.3s ease-in; to have a fade-in effect. Then there are two little helper functions that are fairly self explanatory so I’ll skip talking about them.

Last thing, to make the Intersection Observer work all browsers, I used the intersection-observer polyfill. I simply import it before any other module so it can check the window object before executing my component code.

You can easily generalise this idea to add this functionality to any other component you want. In anyway be aware not to put any computationally heavy set of operations in the callback of the observer as the documentations says :

Be aware that your callback is executed on the main thread. It should operate as quickly as possible

So as you see it was very simple writing a little component to do lazy loading. I hope you learned something new.

Written by

Developer in Berlin, like javascript, playing with algorithms 𝚯, photography 📷, novels 📖

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store