61

How to preload images in React.js? I have dropdown select component which works like menu , but i have to preload image icons for items,because sometimes they are not visible on first open.

I have tried:

https://github.com/sambernard/react-preload

https://github.com/wizardzloy/react-img-preload

First one has nice API easy to understand and use ,but is spaming console with warning that images were not loaded even when they were. Second one has strange API ,but I tried example and it did not preload anything.

So I probably need to implement something at my own ,but do not know where to start. Or another possibility would be to loaded them with webpack.

Ľubomír
  • 1,075
  • 1
  • 12
  • 20

18 Answers18

86

Supposing you have pictures: string[]; - array of pictures urls defined in your component's props. You should define componentDidMount() method in your component and then just create new Image object for each picture you want to be preloaded:

componentDidMount() {
    this.props.pictures.forEach((picture) => {
        const img = new Image();
        img.src = picture.fileName;
    });
}

It forces browser to load all images.

ICW
  • 4,875
  • 5
  • 27
  • 33
  • 4
    If you're using JS replace "let" with "const". Don't use "var" in this scenario – Daniel Reina Aug 20 '18 at 11:59
  • 15
    One-liner: `new Image().src = picture.fileName;` instead of two lines – Maxim Mazurok Apr 15 '19 at 17:15
  • @DanielReina What is the benefit of using `const` over `let`? – kojow7 Aug 20 '19 at 15:49
  • 4
    @kojow7 TL;DR use "const" if you know the variable you're declaring won't change and get some perks for free :) Some of the advantages. Using const allows the compiler to make some optimisations since the type and value won't change. It also improves the readability of the code because you know the value won't change. It'll give you a TypeError if you change the value in a different place in your code, which might help avoiding bugs where you change a value you're not supposed to change – Daniel Reina Aug 21 '19 at 08:01
  • When I try this, if I click a button to load a different set of images while these images are still in the middle of loading, or even another component with images, these images will not stop loading. This slows down the new images from loading right away. How do I tell the previous images to stop loading? – kojow7 Sep 20 '19 at 03:36
  • 4
    How do you use the image created by new Image() in your markup? – leto Nov 02 '19 at 16:10
  • 5
    @mistarsv You can't with react unless you do something unsafe. It does load the images into the cache so you can use an img tag like normal with the same source and it will load instantly. – Charles Nov 18 '19 at 23:36
  • @Lulchenko Aleksey Thanks a lot this really helped me. I got suck on how to load multiple images before using them in order to prevent internet connection issues. – Patrissol Kenfack Jan 22 '20 at 07:20
  • How does this impact Lighthouse loading scores? Does this delay the completion of LCP or TTI? – Clayton Rothschild Feb 28 '22 at 18:45
  • This doesn't work in Firefox, as at version 109. See my answer below. – Dale Anderson Feb 16 '23 at 22:22
16

This is a semi-full example of the React Hooks (with TypeScript) way of pre-loading images inside a component. You can also make this as a hook.

In these examples we use useEffect and useState here, but the actual pre-loading work is inside the preloadImage() function we have. Notice also that importing images you will get a string.

Adding directly inside a component

import React, { useEffect, useState } from 'react'

import Image1 from 'images/image1.png'
import Image2 from 'images/image2.jpg'
import Image3 from 'images/image3.svg'

const preloadSrcList: string[] = [
  Image1,
  Image2,
  Image3,
]

function preloadImage (src: string) {
  return new Promise((resolve, reject) => {
    const img = new Image()
    img.onload = function() {
      resolve(img)
    }
    img.onerror = img.onabort = function() {
      reject(src)
    }
    img.src = src
  })
}

export default function Component() {
  const [assetsLoaded, setAssetsLoaded] = useState<boolean>(false)

  useEffect(() => {
    let isCancelled = false

    async function effect() {
      if (isCancelled) {
        return
      }

      const imagesPromiseList: Promise<any>[] = []
      for (const i of preloadSrcList) {
        imagesPromiseList.push(preloadImage(i))
      }
  
      await Promise.all(imagesPromiseList)

      if (isCancelled) {
        return
      }

      setAssetsLoaded(true)
    }

    effect()

    return () => {
      isCancelled = true
    }
  }, [])

  if (!assetsLoaded) {
    return <p>Preloading Assets</p>
  }

  return <p>Assets Finished Preloading</p>
}

Better as a hook: useImagePreloader()

import { useEffect, useState } from 'react'

function preloadImage (src: string) {
  return new Promise((resolve, reject) => {
    const img = new Image()
    img.onload = function() {
      resolve(img)
    }
    img.onerror = img.onabort = function() {
      reject(src)
    }
    img.src = src
  })
}

export default function useImagePreloader(imageList: string[]) {
  const [imagesPreloaded, setImagesPreloaded] = useState<boolean>(false)

  useEffect(() => {
    let isCancelled = false

    async function effect() {
      console.log('PRELOAD')

      if (isCancelled) {
        return
      }

      const imagesPromiseList: Promise<any>[] = []
      for (const i of imageList) {
        imagesPromiseList.push(preloadImage(i))
      }
  
      await Promise.all(imagesPromiseList)

      if (isCancelled) {
        return
      }

      setImagesPreloaded(true)
    }

    effect()

    return () => {
      isCancelled = true
    }
  }, [imageList])

  return { imagesPreloaded }
}

Using useImagePreloader() inside your component

import React, { useEffect, useState } from 'react'
import useImagePreloader from 'hooks/useImagePreloader'

import Image1 from 'images/image1.png'
import Image2 from 'images/image2.jpg'
import Image3 from 'images/image3.svg'

const preloadSrcList: string[] = [
  Image1,
  Image2,
  Image3,
]

export default function Component() {
  const { imagesPreloaded } = useImagePreloader(preloadSrcList)

  if (!imagesPreloaded) {
    return <p>Preloading Assets</p>
  }

  return <p>Assets Finished Preloading</p>
}
Jo E.
  • 7,822
  • 14
  • 58
  • 94
  • 2
    This is a great example of a use case where useEffect is a massive step back from the lifecycle methods. It used to be so simple. componentDidMount, done. Now it's this verbose, hacky mess. That's not Jo E's fault, it's React. – Matt West Jan 05 '22 at 00:23
  • Jo, after preloading the images, how does one display them? – Rylan Schaeffer Jun 03 '22 at 16:58
  • 1
    @RylanSchaeffer No difference on display. Just make sure it is after the `imagesPreloaded` check. https://stackoverflow.com/questions/44154939/load-local-images-in-react-js – Jo E. Jun 06 '22 at 10:29
8

If you have some images still reloading after being cached, try to store them in the window object :

componentDidMount() {
    const imagesPreload = [image1, image2, image3];
    imagesPreload.forEach((image) => {
        const newImage = new Image();
        newImage.src = image;
        window[image] = newImage;
    });
}

(you don't need to call images from the window object)

DevAb
  • 530
  • 6
  • 12
6

react hook way

useEffect(() => {
  //preloading image
  faceArray.forEach((face) => {
    const img = new Image();
    img.src = face;
  });
}, []);
Facundo Colombier
  • 3,487
  • 1
  • 34
  • 40
Anil Kumar
  • 465
  • 4
  • 10
  • This work only when you have the `src`, if you are importing using `image-loader`, then you don't have it , how do you do in that case ? – Dimitri Kopriwa Dec 26 '20 at 14:51
  • 1
    When I do this, the image is reloaded via another network request in React dom when the image component is rendered. – coler-j Dec 16 '21 at 14:25
  • You can try storing the image into the state prior to rendering the images, and use the id attribute on the image tag. @coler-j – Anil Kumar Dec 16 '21 at 19:57
4

This works:

import im1 from 'img/im1.png'
import im2 from 'img/im2.png'
import im3 from 'img/im3.png'

componentDidMount() {
    imageList = [im1, im2, im3]
    imageList.forEach((image) => {
        new Image().src = image
    });
}
DevB2F
  • 4,674
  • 4
  • 36
  • 60
4

Here's a way to put this functionality into a custom hook that you can use in any of your functional components.

An important part that some answers are missing is to store a reference to the preloaded images in something like the window object. If you don't do this, you will probably find that the browser will re-request the images once they're actually needed despite the fact that they were already preloaded.

The first time you use this hook, a key will be added to window called usePreloadImagesData and it will be initialized to {}.

Each time the hook is used in a component, a new key will be added to window.usePreloadImagesData. The key's name is randomly generated. It is initialized to [].

The hook takes an array of image source strings. For each image source, a new Image() is created and added to the window.usePreloadImagesData[key] array.

When the component is unloaded, or the array passed to the custom hook changes, window.usePreloadImagesData[key] will be deleted to avoid a memory leak.

This example is written in Typescript.

import { useEffect } from 'react';

declare global {
  interface Window {
    usePreloadImagesData?: Record<symbol, unknown[]>;
  }
}

export const usePreloadImages = (imageSrcs: string[]): void => {
  useEffect(() => {
    const key = Symbol();
    window.usePreloadImagesData = window.usePreloadImagesData ?? {};
    window.usePreloadImagesData[key] = [];
    for (const src of imageSrcs) {
      // preload the image
      const img = new Image();
      img.src = src;
      // keep a reference to the image
      window.usePreloadImagesData[key].push(img); 
    }
    return () => {
      delete window.usePreloadImagesData?.[key];
    };
  }, [ imageSrcs ]);
};

When using this hook, ensure that the array of image source strings passed to the hook is constant; otherwise it will cause unnecessary re-rendering. I.e., either create it outside the component or use something like React.useMemo.

E.g.

import React from 'react';
import { usePreloadImages } from '../hooks/usePreloadImages';

import img1 from './image-1.jpg';
import img2 from './image-2.jpg';

const preload = [ img1, img2 ]; // create constant array here, outside the component

export const MyComponent: React.FC = () => {

  usePreloadImages(preload); // correct
  // usePreloadImages([ img1, img2 ]); // incorrect

  return (
    // ...
  );
};
Dave
  • 1,918
  • 1
  • 16
  • 25
3

If its only about delivering few small "icons" - (why not using fonts?) - and if the server serves files gzipped you could use base64 for example.

Otherwise if the select is not instantly visible you could also add img tags (with display: none) to the previous HTML. Another way would be to append Image objects to the DOM and wait for .onload before displaying the component (this approach is used by the libraries you mentioned).

As far as I can imagine webpack or react can't do anything special for you here. This is something on client side and these are just the tools to implement your own preloading API (or even use the existing APIs in JS/TS, React, Angular, ......)

Tim
  • 2,236
  • 1
  • 20
  • 26
  • 4
    There are [**plenty** of reasons](https://css-tricks.com/icon-fonts-vs-svg/) to use svgs instead of fonts. – filoxo Mar 06 '17 at 00:30
  • You could add the option of inline SVGs as well (React makes such an icon system easy to implement). Also, if the author decides to go with data URI, webpack makes that easy. But yeah, the question is not specific enough. – silvenon Mar 06 '17 at 00:32
  • Thought about fonts which refers to svgs? But thanks for the hint Sure there are many tools which can help you to implement such stuff.. – Tim Mar 06 '17 at 00:33
  • Well these icons are in png format already on server, therefore I have no way to convert them into svg without loosing quality. I can not set display:noone, because for dropdown I am using react-select library which handles yourself what is appended or visible – Ľubomír Mar 06 '17 at 02:25
1

All of these duplicate answers do not work in Firefox (as at version 109). You need to actually mount the image into the DOM:

const preloadImage = src => new Promise(resolve => {
  const image = new Image()
  image.src = src
  image.style.display = 'none'
  image.onload = () => {
    document.body.removeChild(image)
    resolve()
  }
  image.onerror = reject
  document.body.appendChild(image)
})
Dale Anderson
  • 1,681
  • 16
  • 15
0

Well my friend, you have some options with link rel preload /prefetch and webpack lazyloading chunks.

more information :

https://github.com/GoogleChromeLabs/preload-webpack-plugin

altoqueperro
  • 371
  • 2
  • 4
0

If you only need to preload the image within a single component:

const preloadedSpinner = useRef();

useEffect(() => {
    preloadedSpinner.current = new Image();
    preloadedSpinner.current.src = "my/image/source";
}, []);
GeForce RTX 4090
  • 3,091
  • 11
  • 32
  • 58
0

You can include them in your component with src and set an initial inline style to display: none and just remove the styling for the image to appear according to your state. This way your image will be preloaded but won't be shown until you need it to be shown. Here is an example:

<SliderItem
    style={slideIndex === 1 ? null : { display: 'none' }}
    src={imgSrc}
    alt={'Kitchen furniture'}
    text={'Caption Text'}
    textStyle={classes.text}
    MySlides={classes.MySlides}
  />
  <SliderItem
    style={slideIndex === 2 ? null : { display: 'none' }}
    src={'/bedroom.jpg'}
    alt={'Bedroom furniture'}
    text={'Caption Text'}
    textStyle={classes.text}
    MySlides={classes.MySlides}
  />
  <SliderItem
    style={slideIndex === 3 ? null : { display: 'none' }}
    src={'/living-room.jpg'}
    alt={'Living-room furniture'}
    text={'Caption Text'}
    textStyle={classes.text}
    MySlides={classes.MySlides}
  />
Sargsian
  • 9
  • 4
0

You may use the following custom hook:

import { useEffect, useState } from 'react';

const preloadImage = (src: string): Promise<HTMLImageElement> =>
  new Promise((resolve, reject) => {
    const img = new Image();

    img.onload = () => resolve(img);
    img.onerror = img.onabort = () => reject();
    img.src = src;
  });

const useImagePreloader = (imageList: string[]) => {
  const [imagesPreloaded, setImagesPreloaded] = useState(false);

  useEffect(() => {
    let isCancelled = false;

    const preloadImages = async () => {
      const imagesPromiseList: Promise<HTMLImageElement>[] =  imageList.map((img) =>
        preloadImage(img),
      );

      try {
        await Promise.all(imagesPromiseList);
      } catch (error: unknown) {
        console.error(error);
      }

      if (isCancelled) {
        return;
      }

      setImagesPreloaded(true);
    };

    preloadImages();

    return () => {
      isCancelled = true;
    };
  }, [imageList]);

  return { imagesPreloaded };
};

export default useImagePreloader;
Jdmiguel
  • 61
  • 1
  • 3
0

You can use React suspense to preload images!

Let's preload some images

Honey
  • 2,208
  • 11
  • 21
0
  • In my case I start with an initial src attribute for my images and wanted to delay changing them after the image has been loaded by the browser.
import { useEffect, useState } from 'react';

export const useImageLoader = (initialSrc: string, currentSrc: string) => {
  const [imageSrc, _setImageSrc] = useState(initialSrc);

  useEffect(() => {
    const img = new Image();
    img.onload = () => {
      _setImageSrc(currentSrc);
    };
    img.src = currentSrc;
  }, [currentSrc]);

  return [imageSrc];
};
Thirosh Madhusha
  • 237
  • 2
  • 11
0

For React

    import imageToScroll ...
    const [imageIsLoaded, setImageIsLoaded] = useState(false);
    useEffect(() => {
            let img = new Image();
            img.src = image.imageToScroll;
        }, [imageIsLoaded])
    <img src={image.imageToScroll}
        onLoad={() => setImageIsLoaded(true)}
    />
Esset
  • 916
  • 2
  • 15
  • 17
DEL
  • 1
  • Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Jan 05 '23 at 15:10
0

here's a React hook for preloading images:

import { useEffect, useState } from 'react';

function usePreloadImage(src) {
  const [isLoading, setLoading] = useState(true);
  const [error, setError] = useState('');

  useEffect(() => {
    let isCancelled = false;
    if (src) {
      setLoading(true);
      const img = new Image();
      img.onload = () => {
        if (isCancelled) return;
        setLoading(false);
      };
      img.onerror = (e) => {
        if (isCancelled) return;
        setLoading(false);
        setError(e.message || 'failed to load image');
      };
      img.src = src;
    } else {
      setLoading(false);
    }

    return () => {
      isCancelled = true;
    };
  }, [src]);

  return { src: src ? src : undefined, isLoading, isError: !!error };
}

export default usePreloadImage;
pureth
  • 680
  • 6
  • 9
-2

Assuming you have an images array containing data for each image, can do it in a one-liner

componentDidMount() {
  this.props.images.forEach(image => (new Image().src = image.src));
}
ok_anthony
  • 41
  • 3
-3

This is a very late answer, I had exactly the same issue to render the dropdown images of a React Fluent UI component. The solution was to change the extension of the file from .png to .jpg and obviously update the extension as well in the constant. That's it!