While we tune every kilobyte out of our JavaScript bundles, we often forget to optimize our image loading strategies the same way. We might be sitting looking at a blank screen for several seconds before the hero image loads, giving the background to your white text.
This article is going to show you how you can write a hook that handles your progressive image loading for you!
What's progressive image loading?
Progressive image loading - at least in this context - is loading a very low-resolution version of the image first, while loading the high resolution version in the background. Once the high resolution version is loaded, the images are swapped.
We're going to name our hook useProgressiveImage
, and pass it an object of a src
prop and a fallbackSrc
prop. It will return the best available image src already loaded, or null
if neither has loaded yet.
function useProgressiveImage({ src, fallbackSrc }) {
return null;
}
We can pre-load images like this by creating a new Image
instance, and setting its src
attribute. We can listen to its onload
event, and react to it accordingly. Let's write out some of this boilerplate code:
function useProgressiveImage({ src, fallbackSrc }) {
const mainImage = new Image();
const fallbackImage = new Image();
mainImage.onload = () => {}; // Still todo
fallbackImage.onload = () => {}; // Still todo
mainImage.src = src;
fallbackImage.src = fallbackSrc;
return null;
}
This is going to run on every render though - which is going to trigger a ton of useless network requests. Instead, let's put it inside a useEffect
, and only run it when the src
or fallbackSrc
props change.
function useProgressiveImage({ src, fallbackSrc }) {
React.useEffect(() => {
const mainImage = new Image();
const fallbackImage = new Image();
mainImage.onload = () => {}; // Still todo
fallbackImage.onload = () => {}; // Still todo
mainImage.src = src;
fallbackImage.src = fallbackSrc;
}, [src, fallbackSrc]);
return null;
}
Next, we need to keep track of which image has been loaded. We don't want our fallback image to "override" our main image if that would load first (due to caching or just coincidence), so we need to make sure to implement that.
I'm going to keep track of this state with the React.useReducer
hook, which accepts a reducer function. This reducer function accepts the previous state (loaded source), and returns the new state depending on what kind of action we dispatched.
function reducer(currentSrc, action) {
if (action.type === 'main image loaded') {
return action.src;
}
if (!currentSrc) {
return action.src;
}
return currentSrc;
}
function useProgressiveImage({ src, fallbackSrc }) {
const [currentSrc, dispatch] = React.useReducer(reducer, null);
React.useEffect(() => {
const mainImage = new Image();
const fallbackImage = new Image();
mainImage.onload = () => {
dispatch({ type: 'main image loaded', src });
};
fallbackImage.onload = () => {
dispatch({ type: 'fallback image loaded', src: fallbackSrc });
};
mainImage.src = src;
fallbackImage.src = fallbackSrc;
}, [src, fallbackSrc]);
return currentSrc;
}
We've implemented two types of actions here - when the main image is loaded and when the fallback image is loaded. We leave the business logic to our reducer, which decides when to update the source and when to leave it be.
What's with the action types?
If you're like me, you're used to reading action types in
CONSTANT_CASE
or at the very leastcamelCase
. Turns out, however, you can call them exactly what you want. I was feeling playful here, and just wrote out the intent. Because why not? 😅Since they're only internal to this little hook anyways, it truly doesn't matter much anyhow.
Using our hook is pretty straight forward too.
const HeroImage = props => {
const src = useProgressiveImage({
src: props.src,
fallbackSrc: props.fallbackSrc
});
if (!src) return null;
return <img className="hero" alt={props.alt} src={src} />;
};
I've created a CodeSandbox you can check out and play with if you want!
Thanks for reading my little mini-article! I always appreciate a share or like or comment, to let me know whether I should keep these coming or not.
Until next time!
Top comments (6)
Nice article, but would be way better if it wouldnt show fallback if main image is cached
I was pretty sure it’s working that way now?
I am fairly certain it is as well, as long as you are handling the image load properly, servers and browsers will handle that issue.
I really liked ur article, but i am struggling to make it work with array of images, could you please take a look at my codesandbox? codesandbox.io/s/image-fallback-cvvbf Do you have any suggestions?
Hi!
Looks like you're using a the
useProgressiveImage
hook inside of a loop, which violates the rules of hooks.Instead, create a new component
ProgressiveImage
, which calls this hook for each image. It'll look like this:it worked, thank you:)