React Lazy Images
Components and utilities for lazy image loading in React.
Table of Contents
Features
What it does not do by itself:
- Polyfill
IntersectionObserver
. Adding polyfills is something you should do consciously at the application level. See Polyfilling IntersectionObserver for how to do this. - Dictate the kind of placeholders displayed. There are many ways to do it; you can use a simple box with a background color, a low-resolution image, some gradient, etc.
In other words, this library focuses on loading the images once in view and supporting loading patterns around that.
The presentational patterns are yours to decide!
Fear not though, we cover both patterns in the examples section.
Install
This package is distributed via npm.
$ npm install --save react-lazy-images
# or
$ yarn add react-lazy-images
Then import according to your modules model and bundler, such as Rollup and Webpack:
import { LazyImage } from "react-lazy-images";
const { LazyImage } = require("react-lazy-images");
A UMD version is also available on unpkg:
<script src="https://unpkg.com/react-lazy-images/dist/react-lazy-images.umd.js"></script>
Motivation
Browsers preload images; as soon as they encounter an <img>
tag with a valid src
, they kick off the request for the image (they even do this before the HTML has been parsed).
Even in cases where a certain image is not in the viewport, it will be requested.
This can have adverse effects for users, especially on mobile or metered connections.
This brings us to the basic premise of any Lazy Image Loading library:
- Have a way to observe the visibility of the DOM elements
- Prevent the browser from loading images directly
- Once an image is in view, instruct the browser to load it and place it in the element
In vanilla JS, this means "hiding" the actual src
in a data-src
attribute, and using classes to indicate state, e.g. .isLazyLoaded .lazyLoad
.
On initialisation, a script queries for these classes and attributes, keeps track of visibily, and swaps data-src
with an actual src
, kicking off the browser request process.
It can elect to preload the Image, and only swap once loaded.
With React, all this implicit state management is brought into one place, since you do not have to stash loading information in the DOM and pick it back up again.
This can potentially mean a nicer, more composable codebase, and it was one of the main design goals for this library.
The way to do this visibility tracking has for the most part been listening for events such as scroll.
This is synchronous by nature and can have performance implications.
It also involves calling getBoundingClientRect()
to calculate the interesection of the image with the viewport; this function causes relayout.
This was the motivation for browsers providing IntersectionObserver.
Using this API is not specific to React; it just seems like a good fit for this task nowadays.
Usage
Quick Start
If you want to just dive in, do this:
import { LazyImage } from "react-lazy-images";
<LazyImage
src="/img/porto_buildings_large.jpg"
alt="Buildings with tiled exteriors, lit by the sunset."
placeholder={({ imageProps, ref }) => (
<img ref={ref} src="/img/porto_buildings_lowres.jpg" alt={imageProps.alt} />
)}
actual={({ imageProps }) => <img {...imageProps} />}
/>;
:warning: It is important that you pass on the ref in placeholder, otherwise the detection of the element intersecting is impossible. :warning:
Note that while you can set the rendered components to be anything you want, you most likely want to use the same src
, srcSet
and alt
attributes in an <img>
eventually.
To keep this consistent, and reduce repetition, the render callbacks pass those attributes back to you.
You can play around with this library on Codesandbox.
Additionally, make sure you understand how to polyfill IntersectionObserver and strategies for when JS is not available.
From then on:
- If you want to learn more about the API and the problem space, read the rest of this section.
- If you need more fine-grained rendering, read about
LazyImageFull
. - If you want to list the props, see the API reference.
Customising what is displayed
The render prop pattern is used throughout in LazyImage
.
The LazyImage
component handles the behaviour of tracking when the image is in view, but leaves the actual rendering up to the consumer.
Thus, whether you want to display a simple <img>
, your own <Image>
, or even wrapped elements, it is simple to do so:
<LazyImage
src="/img/porto_buildings_large.jpg"
alt="Buildings with tiled exteriors, lit by the sunset."
placeholder={
({imageProps, ref}) =>
<img ref={ref} src="/img/porto_buildings_lowres.jpg" alt={imageProps.alt} />
}
actual={
({imageProps}) =>
<img {...imageProps} />
}
/>
<LazyImage
src="/img/porto_buildings_large.jpg"
alt="Buildings with tiled exteriors, lit by the sunset."
placeholder={
({imageProps, ref}) =>
<div ref={ref} className={'LazyImage-Placeholder'}>
<img src="/img/porto_buildings_lowres.jpg" alt={imageProps.alt} />
</div>
}
actual={
({imageProps}) =>
<div className={'LazyImage-Actual'}>
<img {...imageProps} />
</div>
}
/>
These props are there to instruct the component what to render in those places, and they take some useful information (in this case, a className) from the LazyImage.
More control with LazyImageFull
LazyImage
should work for most cases, but you might need more fine-grained rendering.
One use case would be doing animations with CSS transitions, where re-rendering the component (which LazyImage
does) would not be sufficient.
In those cases, consider LazyImageFull
:
import { LazyImageFull, ImageState } from "react-lazy-images";
<LazyImageFull src="/img/porto_buildings_large.jpg">
{({ imageProps, imageState, ref }) => (
<img
{...imageProps}
ref={ref}
src={
imageState === ImageState.LoadSuccess
? imageProps.src
: "/img/porto_buildings_lowres.jpg"
}
style={{ opacity: ImageState.LoadSuccess ? "1" : "0.5" }}
/>
)}
</LazyImageFull>;
This component takes a function as a child, which accepts {src, srcSet, imageState}
.
The various image states are imported as {ImageState}
, and you can conditionally render based on them.
This technique can give you more fine-grained rendering if needed, but can potentially be more verbose.
Any of the presentational patterns presented that are possible with LazyImage
are also possible with LazyImageFull
.
(The opposite is not necessarily true, or at least has more duplication).
In fact, if you check src/LazyImage.tsx
, you will see that LazyImage
is implemented in terms of LazyImageFull
!
Load before swap
A common optimisation to the loading strategy is to preload the image before swapping it for the placeholder.
In other words, once the image is in view, you can kick off a request to load the image, and only show it once fully loaded.
This avoids presenting a half-loaded image (i.e. one that is still scanning top-to-bottom), and makes the transition smoother.
This behaviour is provided with the src
prop:
<LazyImage
src="/img/porto_buildings_large.jpg"
alt="Buildings with tiled exteriors, lit by the sunset."
placeholder={
({imageProps, ref}) =>
<div ref={ref} className={`LazyImage-Placeholder`}">
<img src="/img/porto_buildings_lowres.jpg" alt={imageProps.alt} />
</div>
}
actual={
({imageProps}) =>
<div className={`LazyImage-Actual`}>
<img {...imageProps} />
</div>
}
/>
There is another case if you are using srcset
for your images; LazyImage
needs that information to preload the correct image. You can provide it with the srcSet
prop.
Loading and Error states
You can choose what to display on Loading and Error using the render props loading
and error
:
<div className="bg-light-silver h5 w-100">
<LazyImage
src="/image/brokenimagenotherewhoops.jpg"
alt="Buildings with tiled exteriors, lit by the sunset."
actual={({ imageProps }) => <img {...imageProps} />}
placeholder={({ ref }) => <div ref={ref} />}
loading={() => (
<div>
<p className="pa3 f5 lh-copy near-white">Loading...</p>
</div>
)}
error={() => (
<div className="bg-light-red h-100 w-100">
<p>There was an error fetching this image :(</p>
</div>
)}
/>
</div>
Eager loading / Server-Side Rendering (SSR)
What does SSR even mean in a lazy images context?
If you recall the basic premise, then you will know that we "hide" the intended image and display a placeholder.
For the actual image request to kick off, Javascript has to have loaded, and detected that the image is in the viewport.
In cases where you are server-side rendering, there can be a non-neglible amount of time until Javascript is available (i.e. it has to download, parse, execute).
For those cases, it would be beneficial if we can mark images to render with the intended/final src by default, so that the browser can start requesting them as soon as it gets the HTML.
This behaviour is available by using a loadEagerly
prop:
<LazyImage
loadEagerly
src="/img/porto_buildings_large.jpg"
alt="Buildings with tiled exteriors, lit by the sunset."
placeholder={({ imageProps, ref }) => (
<img ref={ref} src="/img/porto_buildings_lowres.jpg" alt={imageProps.alt} />
)}
actual={({ imageProps }) => <img {...imageProps} />}
/>
While the usage is simple, the patterns in your app will not necessarily be so.
Think about the cases where it is beneficial to do this, and apply it with intent.
Examples might be eager-loading hero images, preloading the first few elements in a list and so on.
Some of these use cases are provided as examples.
Debounce / Delay
In cases where you have a long list of images that the user might scroll through, then loading intermediate images can waste bandwidth and processing time.
This is undesired.
The way to handle it is with a minimum duration that the image has to stay within the viewport, before making the request.
This is specified using the debounceDurationMs
prop:
<LazyImage
src="/img/porto_buildings_large.jpg"
alt="Buildings with tiled exteriors, lit by the sunset."
debounceDurationMs={1000}
placeholder={({ imageProps, ref }) => (
<img ref={ref} src="/img/porto_buildings_lowres.jpg" alt={imageProps.alt} />
)}
actual={({ imageProps }) => <img {...imageProps} />}
/>
Fallback without Javascript
If Javascript is disabled altogether by the user, then they will be stuck with the placeholder (and any images loaded eagerly).
This is probably undesirable.
There are a few strategies for fallbacks.
Most of them are variations on a <noscript>
tag with the actual
img and hiding the placeholder if JS is disabled.
Here is what it looks like rendered:
<noscript>
<style>
.LazyImage {
display: none;
}
</style>
</noscript>
<img class="LazyImage" src="/img/porto_buildings_lowres.jpg" alt="Buildings with tiled exteriors, lit by the sunset." />
<noscript>
<img src="/img/porto_buildings_large.jpg" alt="Buildings with tiled exteriors, lit by the sunset." />
</noscript>
Until v0.3.0, this library had a fallback API, in the form of a fallback
render prop.
This has been disabled due to issues with <noscript>
in React causing the fallback to always load.
(See https://github.com/facebook/react/issues/11423 for more details)
Current solutions involve either using dangerouslySetInnerHTML
, which is not safe for arbitrary library use, or ReactDOMServer.renderToStaticMarkup
.
I thought it would be irresponsible to hide the fact that dangerouslySetInnerHTML
is used from the user, so that excludes the first option.
I also think that using the server method, albeit safe, would be messy with some bundling configurations (which would keep the entirety of react-dom/server
).
Silver lining:
There is generally no case where <noscript>
will be rendered by client-side react.
This means that, if you are in charge of server-rendering and you trust your bundling setup, then you can have this fallback!
Look at src/fallbackUtils.tsx
for a function that can work.
You would probably do something like this:
<LazyImage
src="actualImgSrc"
alt="alt description here"
placeholder={
actual={
/>
<Fallback>
<img src="/img/porto_buildings_large.jpg" alt="Buildings with tiled exteriors, lit by the sunset." />
</Fallback>
Don't forget to also hide the .LazyImage
as shown above.
This may or may not be good enough.
Please open an issue to discuss your needs if that is the case :)
Polyfill IntersectionObserver
IntersectionObserver is generally well-supported, but it is still important to polyfill it!
You can consult the usage data for IntersectionObserver here.
The polyfill itself is pretty small, on the order of 6k min, 2k gzipped.
The polyfill is available through npm:
npm install --save intersection-observer
And import it at your app's entry point:
import "intersection-observer";
Polyfill.io is an alternative method of distributing the polyfill if you wish.
About the polyfill
It is generally a good idea to know what you are adding to your codebase
The polyfill behaviour is to fall back to the older strategy; "debounced scroll listener and calculate bounding rectangle", as mentioned above.
It will not be as performant as the native IntersectionObserver, but likely no worse than most implementations of the older strategy.
Examples
About understanding the library and loading patterns
A variety of usage examples and recipes are provided in the form of Storybook.
You can browse the documentation online or look at stories/
.
Read the notes section either on Storybook or the story source if you are wondering about the specifics of each pattern demonstrated.
About abstracting over it and presentational patterns
The starter on Codesandbox has a good basis for two popular presentational patterns.
In particular, it shows intrinsic placeholders and fading in the actual image.
You might be thinking that this library has a lot of wiring exposed.
This is very much intended.
If this library were to provide presentational patterns out of the box, then it would lead to many issues and PRs about what ultimately is opinion.
Being inclined to fork a library just to add a prop is not a nice situation to be in, compared to writing one abstracted component for your specific use case.
The behaviour and loading patterns are configurable, because those are what this library is about.
The presentation can be derived from those plus, crucially, any specific needs your application has.
API Reference
<LazyImage />
accepts the following props:
Name | Type | Default | Required | Description |
---|
src | String | | true | The source of the image to load |
alt | String | | false | The alt text description of the image you are loading |
srcSet | String | | false | If your images use srcset, you can pass the srcSet prop to provide that information for preloading. |
sizes | String | | false | If your images use srcset, the sizes attribute helps the browser decide which source to load. |
actual | Function (render callback) of type ({imageProps}) => React.ReactNode | | true | Component to display once image has loaded |
placeholder | Function (render callback) of type ({imageProps, ref}) => React.ReactNode | undefined | true | Component to display while no request for the actual image has been made |
loading | Function (render callback) of type () => React.ReactNode | placeholder | false | Component to display while the image is loading |
error | Function (render callback) of type () => React.ReactNode | actual (broken image) | false | Component to display if the image loading has failed (render prop) |
debounceDurationMs | Number | N/A | false | The minimum duration that the image has to be in the viewport before starting to load, in ms. This can help avoid loading images while the user scrolls quickly past them. |
loadEagerly | Boolean | false | false | Whether to skip checking for viewport and always show the 'actual' component |
observerProps | {threshold: number, rootMargin: string} | {threshold: 0.01, rootMargin: "50px 0px"} | false | Subset of props for the IntersectionObserver |
experimentalDecode | Boolean | false | false | Decode the image off-main-thread using the Image Decode API. Test before using! |
<LazyImageFull />
accepts the following props:
Name | Type | Default | Required | Description |
---|
src | String | | true | The source of the image to load |
alt | String | | false | The alt text description of the image you are loading |
srcSet | String | | false | If your images use srcset, you can pass the srcSet prop to provide that information for preloading. |
sizes | String | | false | If your images use srcset, the sizes attribute helps the browser decide which source to load. |
debounceDurationMs | Number | N/A | false | The minimum duration that the image has to be in the viewport before starting to load, in ms. |
loadEagerly | Boolean | false | false | Whether to skip checking for viewport and always show the 'actual' component |
observerProps | {threshold: number, rootMargin: string} | {threshold: 0.01, rootMargin: "50px 0px"} | false | Subset of props for the IntersectionObserver |
children | Function of type ({imageProps, imageState, ref}) => React.ReactNode | | true | Function to call that renders based on the props and state provided to it by LazyImageFull |
experimentalDecode | Boolean | false | false | Decode the image off-main-thread using the Image Decode API. Test before using! |
You can consult Typescript types in the code for more context.
Feedback
I have some specific questions that I would like input on. If you want to go exploring, or have used the library and had gripes with it, then see FEEDBACK.md
and let's have a discussion!
Roadmap
See ROADMAP.md
for information and ideas about where the project is headed.
Contributing
I would love to have contributions on this! Are there more patterns that we can expose and simplify? Is something not clear? See CONTRIBUTING.md
for details.
Thanks and Inspiration
Here are some resources whose ideas resonate with me and have informed this library.
License
MIT License © Fotis Papadogeorgopoulos