Improve Image loading using Suspense and SWR

Dec 28, 2022·5 min read

This post assumes that you are somewhat familiar with the Suspense API and the SWR library, as it will not cover these two things in much detail.

In case you are not familiar with these two terms, here is a short summary:

  • SWR (stale-while-revalidate) is a strategy that first returns data from the cache (stale), then sends a fetch request (revalidate), and finally provides up-to-date data.
  • <Suspense> component allows you to display a fallback until its children have finished loading.

Let's say we want to create a simple app that displays images of all available Dota 2 heroes:

App.tsx
HeroList.tsx
Copy

import { Suspense } from 'react';
import { SWRConfig } from 'swr';
import HeroList from '/components/HeroList';
import { Hero } from '/types/Hero';
import Spinner from '/ui/Spinner.styled';
function App() {
return (
<SWRConfig
value={{
fetcher: (resource, init) =>
fetch(resource, {
...init,
headers: new Headers({
Authorization: `Bearer ${process.env.REACT_APP_STRATZ_API}`,
}),
}).then((res) => res.json()),
suspense: true,
}}
>
<Suspense fallback={<Spinner />}>
<HeroList />
</Suspense>
</SWRConfig>
);
}
export default App;

This is the result:

suspense-before-gif

Not great, right? This will occur on the first load of the app because the images are not cached yet, and we are only displaying a spinner until the data becomes available. Currently, we do not have any logic that listens for image loading. It's time to fix this behavior and use Suspense for image loading as well.

SuspenseImg.tsx
Copy

import {FC} from 'react';
type Cache = Record<string, boolean | Promise<void>>;
type ImgCache = {
__cache: Cache;
read: (src: keyof Cache) => boolean | Promise<void>;
};
type SuspenseImgProps = {
src: string;
alt: string;
height: number;
key: number;
};
const imgCache: ImgCache = {
__cache: {},
read(src) {
if(!this.__cache[src]) {
this.__cache[src] = new Promise((resolve) => {
const img = new Image();
img.onload = () => {
this.__cache[src] = true;
resolve(this.__cache[src]);
}
img.src = src;
}).then(() => {
this.__cache[src] = true;
});
}
if(this.__cache[src] instanceof Promise) {
throw this.__cache[src];
}
return (this.__cache[src]);
}
}
export const SuspenseImg: FC<SuspenseImgProps> = ({ src, alt, ...rest }) => {
imgCache.read(src);
return <img src={src} alt={alt} {...rest} />;
};

This component may appear overwhelming at first glance, but let's break it down and explain what is happening.

Type definition

First we define a type called Cache which is a record of string keys and values that are either boolean or Promise<void>.

The ImgCache type is then defined as an object with a __cache property of type Cache, and a read method that takes a src parameter of type keyof Cache (a string that is a key in the Cache object) and returns a boolean or Promise<void> value.

The SuspenseImgProps type is then defined as an object with the properties src, alt, height, and key, all of which are required. src is a string representing the source URL of the image, alt is a string representing the alternative text for the image, height is a number representing the height of the image in pixels, and key is a number representing a unique identifier for the image.

SuspenseImg.tsx
Copy

type Cache = Record<string, boolean | Promise<void>>;
type ImgCache = {
__cache: Cache;
read: (src: keyof Cache) => boolean | Promise<void>;
};
type SuspenseImgProps = {
src: string;
alt: string;
height: number;
key: number;
};
const imgCache: ImgCache = {
__cache: {},
read(src) {
if (!this.__cache[src]) {
this.__cache[src] = new Promise((resolve) => {
const img = new Image();
img.onload = () => {
this.__cache[src] = true;
resolve(this.__cache[src]);
};
img.src = src;
}).then(() => {
this.__cache[src] = true;
});
}
if (this.__cache[src] instanceof Promise) {
throw this.__cache[src];
}
return (this.__cache[src]);
},
};
export const SuspenseImg: FC<SuspenseImgProps> = ({ src, alt, ...rest }) => {
imgCache.read(src);
return <img src={src} alt={alt} {...rest} />;
};

imgCache object

The imgCache object is then defined as an instance of ImgCache.

It has a __cache property initialized as an empty object and a read method that takes a src parameter.

The method first checks if the src key is not present in the __cache object.

If it is not, the method creates a new Promise that resolves with a value of true when the image has finished loading.

The Promise is assigned as the value for the src key in the __cache object.

If the src key is present in the __cache object and its value is a Promise, the method throws the Promise.

If the src key is present in the __cache object and its value is not a Promise, the method returns the value.

SuspenseImg.tsx
Copy

type Cache = Record<string, boolean | Promise<void>>;
type ImgCache = {
__cache: Cache;
read: (src: keyof Cache) => boolean | Promise<void>;
};
type SuspenseImgProps = {
src: string;
alt: string;
height: number;
key: number;
};
const imgCache: ImgCache = {
__cache: {},
read(src) {
if (!this.__cache[src]) {
this.__cache[src] = new Promise((resolve) => {
const img = new Image();
img.onload = () => {
this.__cache[src] = true;
resolve(this.__cache[src]);
};
img.src = src;
}).then(() => {
this.__cache[src] = true;
});
}
if (this.__cache[src] instanceof Promise) {
throw this.__cache[src];
}
return (this.__cache[src]);
},
};
export const SuspenseImg: FC<SuspenseImgProps> = ({ src, alt, ...rest }) => {
imgCache.read(src);
return <img src={src} alt={alt} {...rest} />;
};

SuspenseImg

Finally, the SuspenseImg functional component is defined as an arrow function that takes a single props parameter of type SuspenseImgProps.

It calls the read method of the imgCache object with the src prop as an argument, and then returns an img element with the src, alt, and other props specified in the props object.

SuspenseImg.tsx
Copy

type Cache = Record<string, boolean | Promise<void>>;
type ImgCache = {
__cache: Cache;
read: (src: keyof Cache) => boolean | Promise<void>;
};
type SuspenseImgProps = {
src: string;
alt: string;
height: number;
key: number;
};
const imgCache: ImgCache = {
__cache: {},
read(src) {
if (!this.__cache[src]) {
this.__cache[src] = new Promise((resolve) => {
const img = new Image();
img.onload = () => {
this.__cache[src] = true;
resolve(this.__cache[src]);
};
img.src = src;
}).then(() => {
this.__cache[src] = true;
});
}
if (this.__cache[src] instanceof Promise) {
throw this.__cache[src];
}
return (this.__cache[src]);
},
};
export const SuspenseImg: FC<SuspenseImgProps> = ({ src, alt, ...rest }) => {
imgCache.read(src);
return <img src={src} alt={alt} {...rest} />;
};

Type definition

First we define a type called Cache which is a record of string keys and values that are either boolean or Promise<void>.

The ImgCache type is then defined as an object with a __cache property of type Cache, and a read method that takes a src parameter of type keyof Cache (a string that is a key in the Cache object) and returns a boolean or Promise<void> value.

The SuspenseImgProps type is then defined as an object with the properties src, alt, height, and key, all of which are required. src is a string representing the source URL of the image, alt is a string representing the alternative text for the image, height is a number representing the height of the image in pixels, and key is a number representing a unique identifier for the image.

imgCache object

The imgCache object is then defined as an instance of ImgCache.

It has a __cache property initialized as an empty object and a read method that takes a src parameter.

The method first checks if the src key is not present in the __cache object.

If it is not, the method creates a new Promise that resolves with a value of true when the image has finished loading.

The Promise is assigned as the value for the src key in the __cache object.

If the src key is present in the __cache object and its value is a Promise, the method throws the Promise.

If the src key is present in the __cache object and its value is not a Promise, the method returns the value.

SuspenseImg

Finally, the SuspenseImg functional component is defined as an arrow function that takes a single props parameter of type SuspenseImgProps.

It calls the read method of the imgCache object with the src prop as an argument, and then returns an img element with the src, alt, and other props specified in the props object.

SuspenseImg.tsx
CopyExpandClose

type Cache = Record<string, boolean | Promise<void>>;
type ImgCache = {
__cache: Cache;
read: (src: keyof Cache) => boolean | Promise<void>;
};
type SuspenseImgProps = {
src: string;
alt: string;
height: number;
key: number;
};
const imgCache: ImgCache = {
__cache: {},
read(src) {
if (!this.__cache[src]) {
this.__cache[src] = new Promise((resolve) => {
const img = new Image();
img.onload = () => {
this.__cache[src] = true;
resolve(this.__cache[src]);
};
img.src = src;
}).then(() => {
this.__cache[src] = true;
});
}
if (this.__cache[src] instanceof Promise) {
throw this.__cache[src];
}
return (this.__cache[src]);
},
};
export const SuspenseImg: FC<SuspenseImgProps> = ({ src, alt, ...rest }) => {
imgCache.read(src);
return <img src={src} alt={alt} {...rest} />;
};


In the end, we can import the SuspenseImg component and replace the img tag with SuspenseImg. This is the final result:

suspense-after-gif

← Prev@next/font tips & tricksNext →Get hooked on Git hooks