DEV Community

Mateusz Charytoniuk
Mateusz Charytoniuk

Posted on

Reliable but complicated and not always suitable way to preload website images

Hello. In this post I want to describe alternative way to preload images without using service workers, storing images in IndexedDB as blobs etc. It allows to display progress bar, etc and reliably loads the image only once even if it's used in several places on the website.

Most of the internet advice is wrong

I'll show a few examples and explain why they do not work as intended.

Wrong way #1

The typical recommended way to preload JS images is something like this:

const image = new Image();

image.onload = function () {
  console.log("preloaded!";
};

image.src = "myimage.png";
Enter fullscreen mode Exit fullscreen mode

Why doesn't it work reliably?

If you do not store the reference to the image element somewhere (in an array, Set etc) and there is any kind of delay between displaying the image, the browser might garbage collect the image object and will download the image again.

Wrong way #2

It is improvement over way #1.

const images = new Set();

const image = new Image();

image.onload = function () {
  console.log("preloaded!";
};

image.src = "myimage.png";

images.add(image);
Enter fullscreen mode Exit fullscreen mode

It stores the reference in the JS object, so most probably after you download the image again it will be loaded from the browser cache.

...but if you download the image in JS and then reference it in a stylesheet like this it still will be downloaded again:

body {
  background-image: url("/myimage.png");
}
Enter fullscreen mode Exit fullscreen mode

See this for further reference:
https://github.com/CreateJS/PreloadJS/issues/253#issuecomment-362316407

How it can be done?

This is not a straightforward way, nor it's easy to maintain, but it would work reliably. So sometimes it might be useful.

First you need to preload image as above, but instead of just stopping there, you can convert the image to dataURI and then append it to the website as a CSS string:

function appendStyles(dataUrl) {
  const style = document.createElement("style");

  style.textContent = `
    .my-element {
      background-image: url(${dataUrl});
    }
  `;

  document.body.appendChild(style);
}
Enter fullscreen mode Exit fullscreen mode
if (myCacheLayer.has("myimage.png")) { 
  // do not load the image, just append the styles
  appendStyles(myCacheLayer.get("myimage.png");

  return;
}

const canvas2d = document.createElement("canvas2d");
const context2d = canvas2d.getContext("2d");

if (!context2d) {
  throw new Error("Unable to obtain canvas context.");
}

const image = new Image();

image.onload = function () {
  canvas2d.width = image.naturalWidth;
  canvas2d.height = image.naturalHeight;

  context2d.drawImage(image, 0, 0);

  const dataUrl = canvas.toDataURL();

  myCacheLayer.set("myimage.png", dataUrl);
  appendStyles(dataUrl);
};

image.src = "myimage.png";

images.add(image);
Enter fullscreen mode Exit fullscreen mode

This way you can add some caching layer to store data URLs or generate progress. The approach above is not suitable to for all use cases and is clunky, but I found it useful when developing web games, where I need to preload all assets on the loading screen. It's more difficult to maintain and results in chunks of CSS code here and there, but hey, it reliably works, so you might also find use case for that, or at least avoid preloading images with just new Image(...); approach.

Top comments (0)