DEV Community

loading...
Cover image for Lazy Loading Image - [1/2]

Lazy Loading Image - [1/2]

didof profile image Francesco Di Donato ・4 min read

Abstract

In this mini-series consisting of two posts I will build a React Component Image which, using custom hooks, shows a low-resolution image that is immediately replaced when the high-resolution counterpart is completely downloaded. In the second phase, I will take care of postponing the download of the second only when the component becomes visible

demo

Repo 📑

Table of content

  1. Low-resolution & High Resolution
  2. High-resolution only when is visible

Low-resolution & High-resolution

Concept

The rendering of a high-resolution image can take - especially for slow connections - several seconds. This lack of readiness results in worse UX

In this post, I deal with solving the problem by building a component that in addition to the high-resolution image source receives one for the low-resolution image to be shown as a replacement until the first is fully downloaded and available

In the next post, I will take care of postponing the download of the high-resolution image only when the component becomes visible within the view. Regardless, the user will not see a missing image as the relative low resolution will already be present

Process

In a project generated via create-react-app I delete all that is superfluous

Then I initialize the construction of the Image component

mkdir src/components
touch src/components/Image.jsx
Enter fullscreen mode Exit fullscreen mode

It is actually two <img> placed one above the other and made visible alternately. To make them superimposable it is sufficient to use a wrapper with the necessary CSS properties. Furthermore, since the two images may have different sizes, it is recommended that while a wrapper defines width and height, the images contained therein adapt to its directives

Image.js
const Image = ({ width = '100%', height = '100%', lowResSrc, highResSrc }) => {
  const styles = {
    wrapper: {
      position: 'relative',
      width,
      height,
    },
    image: {
      position: 'absolute',
      width: '100%',
      height: '100%',
    },
  }

  return (
    <div style={styles.wrapper}>
      <img src={lowResSrc} style={styles.image} />
      <img src={highResSrc} style={styles.image} />
    </div>
  )
}

export default Image
Enter fullscreen mode Exit fullscreen mode

Inline CSS is used rather than another solution for simplicity's sake

Now I use the component and I provide it with the required props

App.js (but it could be anywhere)
const srcTuple = [
  'https://via.placeholder.com/150',
  'https://via.placeholder.com/600',
]

...

<Image
  width={300}
  height={300}
  lowResSrc={srcTuple[0]}
  highResSrc={srcTuple[1]}
/>
Enter fullscreen mode Exit fullscreen mode

At this point on the screen, there is the image related to srcTuple[0] (the low-resolution source) because that is what the style wants. For the replacement to occur, it is necessary to be able to intervene when the download of the high-resolution image is completed

To do this I can use the onLoad method of the<img> attribute. The explanatory name indicates when it is performed

The question remains of what to actually make it perform


With a view to modern React, I decided to opt for a custom hook
It must keep track of the state of the image loading and on the basis of it return a style that leads to a pleasant transition between the two images of the component. To do this it must expose a method that will be associated with the onLoad method

mkdir src/hooks
touch src/hooks/useImageOnLoad.js
Enter fullscreen mode Exit fullscreen mode
useImageOnLoad.js
import { useState } from 'react'

const useImageOnLoad = () => {
  const [isLoaded, setIsLoaded] = useState(false)

  const handleImageOnLoad = () => setIsLoaded(true)

  const transitionStyles = {
    lowRes: {
      opacity: isLoaded ? 0 : 1,
      filter: 'blur(2px)',
      transition: 'opacity 500ms ease-out 50ms',
    },
    highRes: {
      opacity: isLoaded ? 1 : 0,
      transition: 'opacity 500ms ease-in 50ms',
    },
  }

  return { handleImageOnLoad, transitionStyles }
}

export default useImageOnLoad
Enter fullscreen mode Exit fullscreen mode

So, just integrate the hook into the component. The method is associated with the onLoad on the high resolution <img>tag. The styles returned by the hook must be associated with its <img> tags

Image.js (snellito)
const Image = ({ ... }) => {
  const { handleImageOnLoad, transitionStyles } = useImageOnLoad()

  const styles = {...}

  const lowResStyle = { ...styles.image, ...transitionStyles.lowRes }
  const hightResStyle = { ...styles.image, ...transitionStyles.highRes }

  return (
    <div style={styles.wrapper}>
      <img src={lowResSrc} style={lowResStyle} />
      <img src={highResSrc} style={hightResStyle} onLoad={handleImageOnLoad} />
    </div>
  )
}

export default Image
Enter fullscreen mode Exit fullscreen mode

pulp demo

Considerations

Given the very little use of the network in this demo, to make the effect more appreciable it can be convenient

  • multiply the number of <Image /> components and their contents
  • simulate throttling in the Network tab of the Developer Tools
  • disable cache

Finally, it is true that compared to a simple <img /> with a single source, <Image /> requires a few more bytes to be downloaded (AKA the low-resolution image). However, it's a small price to pay for a better UX, it's so true?


Thanks for reading, continue to the next post 🐨

Repo 📑

If you like it, let's get in touch 🐙, 🐦 and 💼

Discussion (0)

pic
Editor guide