Building a Lazy Loader from Scratch in React (Part 2)
Tags: javascript, react, webdev, frontend
Published: 2023-11-01 05:30:40

## Table of Contents<a name=""></a>

- [Introduction](#Introduction)
- [The problem of CLS](#The_problem_of_CLS)
- [The problem with absolute width](#The_problem_with_absolute_width)
- [First try at responsiveness](#First_try_at_responsiveness)
- [Second try at responsiveness](#Second_try_at_responsiveness)
    - [Using `padding-bottom`](#Using_padding-bottom)
    - [Using `aspect-ratio`](#Using_aspect-ratio)

- [Getting the width and height of images](#Getting_the_width_and_height_of_images)
- [Next steps](#Next_steps)


-----

## Introduction<a name="Introduction"></a><a name="Introduction"></a>

In the previous part of this series, [Building a Lazy Loader from Scratch in React (Part 1)](https://www.codeguage.com/blog/lazy-loader-react-1), we saw the setup of a very basic lazy loader using [React](https://www.codeguage.com/courses/react/) and the modern [`IntersectionObserver` API](https://www.codeguage.com/courses/advanced-js/intersection-observer-introduction).

Just for a quick recap, we crafted a `LazyImage` component taking a `src` prop and applying it to a descendant `<img>` element only when the underlying element is about to show up in the viewport (by merely touching it).

As you can recall, the created lazy loader was exceptionally basic, void of many _good things_.

Now, in this part of the series, we'll be extending that basic lazy loader to prevent it from creating **CLS issues** (more on what that means below) and make it **responsive** (in the sense of responsive designing).

Both of these things are absolutely crucial for the success of any lazy loader:

- CLS issues lead to a poor user experience, and so we should strive hard to reduce such issues wherever possible.
- Creating responsive websites is even more crucial as a large part of web surfers comes from mobile phones whom we don't want to give broken-off webpages.


In this article, we'll be covering a couple of features of CSS — in particular `padding-bottom` (or equivalently, `padding-top`) and `aspect-ratio`, and how they'll come in handy for our lazy loader — and just a ton full of fruitful information in general.

So without any further ado, let's begin the learning.

-----

## The problem of CLS<a name="The_problem_of_CLS"></a><a name="The_problem_of_CLS"></a>

Let's start by opening the example webpage that we created with our elementary lazy loader in the previous installment of this series:

[Live Example](https://www.codeguage.com/courses/react/examples/blog-lazy-loader-1)

First off, we note that the actual height of the loaded image isn't 100px, which is the value that we have given to the `min-height` property of the lazy image's `<div>`.

When the image finally loads, it increases the height of the `<div>` to 424px, which is the original height of the image. This is simply because the image is larger than 100px and, likewise, gets the `<div>` to be stretched along with it to that very height (which is quite a usual behavior).

Anyways, if we put some content after the lazy image in our webpage, this height change, as the lazy image gets loaded, will cause that content to shift down.

This shifting is considered to be a major user experience issue, one that Google recognizes and gives a special place in its [Core Web Vitals](https://web.dev/articles/vitals).

Google calls it **_Cummulative Layout Shift_**, or simply **_CLS_**.

To quote it from web.dev's definition of [CLS](https://web.dev/articles/cls):

> CLS is a measure of the largest burst of layout shift scores for every unexpected layout shift that occurs during the entire lifespan of a page.

CLS issues arise because of a multitude of reasons. Perhaps, the most common is **NOT giving explicit dimensions to images**.

In the example above, the rendered `<img>` element didn't have any explicit width or height set on it; hence, we faced the layout shift problem because of it.

In order to prevent an image from producing a layout shift, we just need to give it a width and height.

Obviously, the first approach that comes to mind, a naive one though, is to set the `width` and `height` attributes on the `<img>` element in order to preset it to a fixed width and height. Later on, when the image finally loads, it'll load within that box that the `<img>` element gets to form because of these attributes.

_Pretty basic, right?_

To restate it: in order to keep our lazy loader from producing a high CLS (which is bad), we just ought to define an explicit width and height for every lazy image.

And for now, we're giving a try to doing so using the `width` and `height` attributes on the `<img>` element rendered by `LazyImage`.

Let's do this for the example that we saw above and see the effect produced.

First up, we'll modify `LazyImage`, incorporating two new props in it, **`width`** and **`height`**, which get applied to the `<img>` element:

```jsx
import { useState, useEffect, useRef } from 'react';

function LazyImage({ src, width, height }) {
   const [srcExists, setSrcExists] = useState(false);
   const divRef = useRef();

   useEffect(() => {
      divRef.current.setSrcExists = setSrcExists;
      LazyImage.io.observe(divRef.current);
   }, []);

   return (
      <div ref={divRef} className="lazy">
         <img width={width} height={height} src={srcExists ? src : null}/>
      </div>
   );
}
```

Next up, we'll clear the `min-height` style from `.lazy` in the CSS and even its `background-color`, since we can now clearly visualize the image prior to its load on the webpage:

```css
.lazy {
   min-height: 100px;
   background-color: #ddd;
}
```

Instead, we'll apply `background-color` directly to the `<img>` element because that's what we ought to visualize:

```css
.lazy img {
   background-color: #ddd;
}
```

With this, we now need to provide these props in our demo webpage:

```jsx
import LazyImage from './LazyImage';

function App() {
   return (
      <>
         <h1>Demonstrating lazy loading</h1>
         <p style={{ marginBottom: 1000 }}>Slowly scroll down until you bring the lazy image into view.</p>
         <LazyImage width={640} height={424} src="/image.jpg" />
      </>
   );
}

export default App;
```

The image that we used in the example is 640px wide and 424px high, hence the values of the `width` and `height` attributes as shown above.

Let's see the result:

[Live Example](https://www.codeguage.com/courses/react/examples/blog-lazy-loader-2)

_Good, but not perfect yet!_

We solved one problem but that at the cost of another one.

-----

## The problem with absolute width<a name="The_problem_with_absolute_width"></a><a name="The_problem_with_absolute_width"></a>

In our example, the image's width is 640px and we've explicitly set the `width` attribute of the `<img>` element to that very value. Said another way, we've set an absolute width on our lazy image.

The consequence of this approach is that when the viewport's width gets less than 640px, the image starts to overflow out of it, leading to a horizontal scroll bar on the webpage.

_This is the problem._

Shown below is an illustration for the webpage that we created above:

![Lazy image overflowing out of the viewport.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vjyjt88z7f3gwh2zvn0v.png)


As far as CLS is concerned, yes, our approach of applying the `width` and `height` attributes does solve it. But as far as responsiveness is concerned, this approach is raising some alarm bells for us to give attention to!

We can't continue using an absolute width on a lazy image as it keeps the lazy loader devoid of being responsive.

Let's think about a better solution.

-----

## First try at responsiveness<a name="First_try_at_responsiveness"></a><a name="First_try_at_responsiveness"></a>

_If we ask you to give us some suggestions on how to make this lazy loader responsive, what will you pitch in?_ Give it a minute of thought.

Assuming you've given it a try, let's now think on it together...

The very first solution that comes to mind to make a lazy image responsive is to give the `<img>` a `100%` value for the `width` CSS property.

As you'd know, this will get the image's width to be precisely as much as the width of its container. The container is the `<div>` element whose width itself turns out to be as much as the available width of its container (which in our example is the `<body>` element). So clearly, this is going to be responsive.

At the same time, we'll also need to let go of the `height` attribute currently set on the `<img>` element so that our image doesn't get distorted.

With the application of `width: 100%`, the image's width will obviously be changing with the viewport's width, and with that, we clearly want the height to be changing as well, in order to maintain the original aspect ratio of the image. When we explicitly specify an absolute height, this can NOT happen; the image's height will remain static throughout, leading to distorted images as the width changes.

Frankly speaking, the last thing you'll want on your website is distorted images.

Anyways, let's see this solution in action.

Here's the `LazyImage` component, with its `width` and `height` props removed since we don't need them, at least for now:

```jsx
import { useState, useEffect, useRef } from 'react';

function LazyImage({ src, width, height }) {
   const [srcExists, setSrcExists] = useState(false);
   const divRef = useRef();

   useEffect(() => {
      divRef.current.setSrcExists = setSrcExists;
      LazyImage.io.observe(divRef.current);
   }, []);

   return (
      <div ref={divRef} className="lazy">
         <img width={width} height={height} src={srcExists ? src : null}/>
      </div>
   );
}
```

Besides this, the styles for `.lazy img` in the CSS will be as follows:

```css
.lazy img {
   background-color: #ddd;
   width: 100%;
}
```

Notice the addition of the `width: 100%` style in there — this will look after making our lazy loader responsive.

As always, let's try the solution:

[Live Example](https://www.codeguage.com/courses/react/examples/blog-lazy-loader-3)

The first thing to realize in the example is that the image is being stretched to fill up the viewport, a courtesy of the `width: 100%` style that we just applied, even though its original width is 640px.

Before we proceed any further, let's change this.

And the simplest way to do so is to set a maximum width on the image. This maximum width will simply be the original width of the image.

If the image's container is larger than the maximum width of the image (which is original width), the image renders at its maximum width. Otherwise, the image renders at the width of its container.

Here's what `LazyImage` becomes now, bringing back only the `width` prop:

```jsx
import { useState, useEffect, useRef } from 'react';

function LazyImage({ src, width }) {
   const [srcExists, setSrcExists] = useState(false);
   const divRef = useRef();

   useEffect(() => {
      divRef.current.setSrcExists = setSrcExists;
      LazyImage.io.observe(divRef.current);
   }, []);

   return (
      <div ref={divRef} className="lazy">
         <img style={{ maxWidth: width }} src={srcExists ? src : null}/>
      </div>
   );
}
```

The `maxWidth` style property is set on the `<img>` element. If you want to, you can instead set it on the `<div>` element as well.

Let's now witness the solution:

[Live Example](https://www.codeguage.com/courses/react/examples/blog-lazy-loader-4)

Time for the final verdict.

Evidently, this `width: 100%` solution does indeed solve our responsiveness issue, as illustrated below:


![Reponsive lazy image fitting into the viewport.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dgeb5m7xedgcvvvjiefj.png)


But unfortunately, it brings back the CLS issue!

When the height isn't explicitly specified, despite the fact that we have `width: 100%` set, we get back to point zero in the drive to make our lazy loader better than where we left it in the [first part of this series](https://www.codeguage.com/blog/lazy-loader-react-1).

_How?_ Here's how.

The `width: 100%` style only gets the image to fill up the entire space of its container; its corresponding height is still not known until the point the image loads. Henceforth, when that happens, that is, when the image finally loads, we get a layout shift. _Oh no._

In short, we can't use _just_ `width: 100%`. Maybe we need something additional with it, or maybe a completely different approach.

Let's think further.

-----

## Second try at responsiveness<a name="Second_try_at_responsiveness"></a><a name="Second_try_at_responsiveness"></a>

As we already know, by virtue of `width: 100%`, the width of the image changes as the viewport's width changes. With this, trivially, its height changes as well.

Likewise, if we have to explicitly specify the height, we need to use something that can _change with the width_.

Setting the height to an absolute value is of no use since that way the image's height would remain the same regardless of its width.

_Can you think of any CSS property to help us here?_

### 1. Using `padding-bottom`<a name="Using_padding-bottom"></a><a name="Using_padding-bottom"></a>

Well, there's an obscure usage of **`padding-bottom`** (or equivalently, of `padding-top`) that can come in handy here.

Recall that we seek a way to specify the height of a lazy image relative to its width. And `padding-bottom` can help us with just this very thing.

When `padding-bottom` is set to a percentage value, for e.g `50%`, it is resolved relative to the width of the container of the underlying element.

So, let's say, the width of an element's container is 500px, and the height of the element itself is 0px as there's no content inside it. Setting `padding-bottom` to `20%` on this element would make its height equal to 100px.

Coming back to our lazy loader, in order to employ the potential of `padding-bottom` to prescribe a height to a lazy image, and thus make it responsive, we need to know the original width and height of the image in advance. Using the width and height, we compute an appropriate value for `padding-bottom`, as a percentage.

The formula is absolutely simple: `(_height_ / _width_) x 100`

_But where to apply this property? Should it go on the `<img>` element or on its containing `<div>`?_

We can't set it on the `<img>` element as it's meant to showcase an image; having `padding-bottom` on it will produce extra spacing below the rendered image, which is clearly undesirable.

This leaves us with the containing `<div>` element. But there's an issue that we face here. If we set `padding-bottom` on `<div>`, it will be resolved relative to its container's width, not the width of the `<div>` itself (as we just learnt above). This will only work fine when the image spans the entire viewport width.

But as we saw above, the image won't always be as large as the viewport, and in that case we'd want to showcase it in its original width.

Therefore, we want `padding-bottom` to act **relative to this width of the image**.

_How to solve this problem?_

Well, we just need to bring in another `<div>` encapsulating our existing `.lazy` element. This `<div>` will get the application of the `max-width` property on it.

Thereafter, the `padding-bottom` property will be applied to `.lazy`; it'll get resolved based on the width of its container which is only as much as the original width of the underlying image.

_Perfect!_

Let's now see an implementation of all this long discussion.

Here's our `LazyImage` component, with both the props `width` and `height` brought back, and a new `<div>` container introduced:

```jsx
import { useState, useEffect, useRef } from 'react';

function LazyImage({ src, width, height }) {
   const [srcExists, setSrcExists] = useState(false);
   const divRef = useRef();

   useEffect(() => {
      divRef.current.setSrcExists = setSrcExists;
      LazyImage.io.observe(divRef.current);
   }, []);

   return (
      <div style={{ maxWidth: width }}>
         <div
            style={{ paddingBottom: `${height / width * 100}%` }}
            ref={divRef}
            className="lazy"
         >
            <img src={srcExists ? src : null}/>
         </div>
      </div>
   );
}
```

The `width` and `height` props are processed by the component to produce a corresponding value for the `padding-bottom` property to be applied to the `<div>`.

Now, since `padding-bottom` gives inner spacing to the `<div>`, if we need the `<img>` inside it to render correctly, without showing up after this whole padding, we need to position it `absolute`ly and get its width and height to be equal to the width and height of its container.

The following CSS takes care of this:

```css
.lazy {
   position: relative;
}

.lazy img {
   background-color: #ddd;
   position: absolute;
   top: 0;
   left: 0;
   width: 100%;
   height: 100%
}
```

Also, notice that we've shifted the `background-color` style from `.lazy img` to `.lazy`.

Here's our demo webpage's code:

```jsx
import LazyImage from './LazyImage';

function App() {
   return (
      <>
         <h1>Demonstrating lazy loading</h1>
         <p style={{ marginBottom: 1000 }}>Slowly scroll down until you bring the lazy image into view.</p>
         <LazyImage width={640} height={424} src="/image.jpg" />
      </>
   );
}

export default App;
```

See how we pass on the original width and height of the image to the `LazyImage` component instance in the `width` and `height` props, respectively.

Let's now run the example and see the output:

[Live Example](https://www.codeguage.com/courses/react/examples/blog-lazy-loader-5)

_Yup, it's flawless._

As desired, the CLS issue hasn't come back because our image's width and height has been predefined in the code. And most importantly, our image is responsive too, without any kind of distortions.

### 2. Using `aspect-ratio`<a name="Using_aspect-ratio"></a><a name="Using_aspect-ratio"></a>

Besides `padding-bottom`, we can also go with the more recent **`aspect-ratio`** property in CSS.

`aspect-ratio` controls the height of an element based on its width. For example, if `aspect-ratio` is set to `16/9` (a common ratio), and if the width of the element is 160px, its height would be 90px.

_Basic stuff, right?_

To use `aspect-ratio` in our lazy loader in place of `padding-bottom`, we still need the `width` and `height` props. However, we don't need the new `<div>` container that we introduced in the section above for `padding-bottom`.

That's precisely because `aspect-ratio` resolves relative to the very element on which it's applied, unlike `padding-bottom` which resolves relative to the parent container of the element.

Here's the definition of `LazyImage` when using `aspect-ratio`:

```jsx
import { useState, useEffect, useRef } from 'react';

function LazyImage({ src, width, height }) {
   const [srcExists, setSrcExists] = useState(false);
   const divRef = useRef();

   useEffect(() => {
      divRef.current.setSrcExists = setSrcExists;
      LazyImage.io.observe(divRef.current);
   }, []);

   return (
      <div
         style={{ aspectRatio: `${width} / ${height}`, maxWidth: width }}
         ref={divRef}
         className="lazy"
      >
         <img src={srcExists ? src : null}/>
      </div>
   );
}
```

Both the `aspect-ratio` and `max-width` CSS properties are applied to the `.lazy` element.

Let's now try the lazy loader:

[Live Example](https://www.codeguage.com/courses/react/examples/blog-lazy-loader-6)

_Works flawlessly._

It's worth noting here that because `aspect-ratio` is a relatively recent property in CSS as compared to `padding-bottom`, it doesn't enjoy as rich of a browser compatibility chart as `padding-bottom`.

So, if you're aiming for a browser-compatible option, we'd recommend you to err on the side of `padding-bottom`, for it's a fairly well supported property in many browsers.

-----

## Getting the width and height of images<a name="Getting_the_width_and_height_of_images"></a><a name="Getting_the_width_and_height_of_images"></a>

Before we end this discussion, we'd like to shed some light on the `width` and `height` props of our `LazyImage` component and where exactly do we get them for an image.

First off, note that JavaScript can NOT help us get these natural dimensions of an image, at least not when it hasn't been loaded — recall that we need the dimensions of the image right from the beginning.

There is only one path to take here: get the image's dimensions from the underlying file system and pass on the obtained values respectively into the `width` and `height` props of `LazyImage`.

This is the only way to prevent a network round trip to the end server just to get to know of the natural dimensions of an image.

-----

## Next steps<a name="Next_steps"></a><a name="Next_steps"></a>

At this point, we have significantly improved our lazy loader from where we left it in the first part of this series.

We've now successfully made the lazy loader not pose any CLS problems (as was the case earlier when we didn't have custom dimensions set on lazy images) and also made is responsive.

As you'd agree, this required quite some effort on our side.

In the next part of this series, we'll be adding another feature to our lazy loader, one that'll make it look way more professional that it currently is. That feature is of a **_fade-in effect_**, given as lazy images load into view.

But for now, let's take a break, for part 3 is to be continued...

-----

## More from CodeGuage

In the meanwhile, you can consider going through some of the following articles published by us on Dev 👇:

{% embed https://dev.to/codeguage/150-questions-to-practice-javascripts-foundational-concepts-1ong %}

{% embed https://dev.to/codeguage/is-frontend-development-easier-than-backend-h77 %}

{% embed https://dev.to/codeguage/how-to-set-up-rollup-for-a-react-application-2322 %}

{% embed https://dev.to/codeguage/4-incredible-vs-code-features-that-you-cant-miss-1nf4 %}