8000 GitHub - buildcom/react-lazy-images: Components and utilities for lazy image loading in React
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content
This repository was archived by the owner on Aug 1, 2023. It is now read-only.

buildcom/react-lazy-images

 
 

Repository files navigation

React Lazy Images

Note: This is a fork of https://github.com/fpapado/react-lazy-images

The entire purpose of this fork is to allow us to publish changes that have been merged to the main repo, but which have not been released.

Components and utilities for lazy image loading in React.

npm gzip size npm downloads dependencies

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:

// ES Modules
// For all possible functions to import look at the documentation
import { LazyImage } from "react-lazy-images";

/// CommonJS modules
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} />}
/>;

⚠️ It is important that you pass on the ref in placeholder, otherwise the detection of the element intersecting is impossible. ⚠️

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."
  // This is rendered first, notice how the src is different
  placeholder={
    ({imageProps, ref}) =>
      <img ref={ref} src="/img/porto_buildings_lowres.jpg" alt={imageProps.alt} />
  }
  // This is rendered once in view; we use the src and alt above for consistency
  actual={
    ({imageProps}) =>
      <img {...imageProps} />
  }
/>

// Perhaps you want a container?
<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";

// Function as child
// `src`, `alt` and `srcSet` are passed back to the render callback for convenience/consistency
<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 ahead and threshold

Further control over the Intersection Observer can be provided through the observerProps prop object:

import { LazyImage } from "react-lazy-images";

<LazyImage
  src="/img/porto_buildings_large.jpg"
  alt="Buildings with tiled exteriors, lit by the sunset."
  placeholder={/* the usual */}
  actual={/* the usual */}
  observerProps={{
    rootMargin: "100px 0",
    threshold: 0.3
  }}
/>;

rootMargin: Margin around the window. This can have values similar to the CSS margin property, e.g. "10px 20px 30px 40px" (top, right, bottom, left) (defaulted to "50px 0px") This can provide control if you want to request your image a certain number of pixels ahead of where the user is scrolling.

threshold: Number between 0 and 1 indicating the percentage that should be visible before a request is sent. (defaulted to 0.01)

(See https://github.com/thebuilder/react-intersection-observer#api)

Load before swap