A progressive image loader in React
Have you ever noticed how the images look when a Medium blog post is loading? If not, reload this page now and have a look. If you’re on a fast wifi you may not have noticed, but if you’re on a so-so mobile connection, you probably noticed that the image was blurry at first and then nicely faded once the final image was loaded.
I’m in the middle of building a web app that will hopefully become the fastest loading burger site in the world when it’s done. 😄 Ironically it’s currently hosted on the free tier at Heroku (which I freaking ❤️ btw) so the app dyno falls a sleep after ~ 30 minutes of inactivity and therefore appears very slow when waking up. You can check it out here.
The app is a Server Side Rendered Progressive Web Application built with React and Next.js. It currently has the following stats after Google Lighthouse and Test My Site has performed their audits, which I’m very happy with, but it still has a few things to implement and tweak to become that ultra super über blazing fast site I aim for.
But enough about the app and back to the topic of loading images like Medium. There are many different ways to make an image load fast but also in a stylish way. The first thing you want to do is make the image size as small as possible by reducing its quality. For instance on my full screen burger jpg’s I’ve reduced the quality to around 70% without any noticeable effect on the image. It did however have a noticeable effect on the file size. You can also save the jpg as progressive.
How to do progressive image loading?
So what does Medium do that takes image loading several steps further? Basically, and a little bit simplified, the following things:
- Initially load a tiny preview image, which is a very, very small version of the final image, around 20x20px
- Blow its width and height up to same size as the final image, maybe 800x600px or even full screen
- Apply a css blur filter to it
Now we have the blurry preview image in place. A preview image can be either an external image resource to request over the network, or even better an inlined image (data URI) to save us from making an extra roundtrip to server.
4. Load the final image in the background
5. Once the final image is loaded, throw away the preview image and replace it with the final one
6. Apply css transition to the filter and remove blurryness
Simple, yet powerful way to smoothen the user experience when loading an image.
But how to do it in React?
Ok, ok! That’s all good in theory, but how on earth do we do it for real, in a declarative React way? Well, it turns out it’s a piece of cake thanks to the nice lifecycle methods that a React component has to offer.
I’m imagining a nice usage of the ProgressiveImage component would look something like this. A preview image and a final image is basically everything that it needs.
Ok, ok again. Nothing fancy so far so lets open up ProgressiveImage component and see how it looks.
A class with an internal state which is the current image to display and a flag to indicate if we’re loading the final image or not. The current image is initially set to the preview image.
When component is mounted we immediately start fetching the final image.
Fetching an image programmatically can be done by creating an Image object and setting the src to the url. We’re also adding an onload event handler to be informed once the browser has finished downloading the image. At that point we update the internal state in the component, changing currentImage from preview to final image and toggling loading flag. And we all know what happens when setState is called. It’s render time!
Render is just rendering the current image, setting css transition and the blur filter if loading is true.
💥 💥 💥 Boom! Awesomeness! 💥 💥 💥
Result in browser when clicking a specific burger.
Here’s the component in one file
Potential future improvements or pull requests 😄
- Support for overriding image style
- Optional render prop which gets passed the image src and loading flag, to delegate complete render to caller component. Something like: