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 is
100% 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 set
threshold : [0.25, 0.5, 0.75] . Then on line 22 we start observing our component
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
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.