From 4379780655e03bf80d0b1d6714c75862b76f9291 Mon Sep 17 00:00:00 2001
From: Brennan Kinney <5098581+polarathene@users.noreply.github.com>
Date: Tue, 28 May 2019 08:35:49 +1200
Subject: [PATCH] chore(gatsby-image): lazy-load refactor (#14158)
Refactor around the recent changes from adding native lazy-loading support.
---
.../src/__tests__/__snapshots__/index.js.snap | 3 +
packages/gatsby-image/src/__tests__/index.js | 9 +-
packages/gatsby-image/src/index.js | 161 +++++++-----------
3 files changed, 70 insertions(+), 103 deletions(-)
diff --git a/packages/gatsby-image/src/__tests__/__snapshots__/index.js.snap b/packages/gatsby-image/src/__tests__/__snapshots__/index.js.snap
index 53c1acf582abc..dbd0f6218016a 100644
--- a/packages/gatsby-image/src/__tests__/__snapshots__/index.js.snap
+++ b/packages/gatsby-image/src/__tests__/__snapshots__/index.js.snap
@@ -27,6 +27,7 @@ exports[` should have a transition-delay of 1sec 1`] = `
crossorigin="anonymous"
height="100"
itemprop="item-prop-for-the-image"
+ loading="lazy"
src="test_image.jpg"
srcset="some srcSet"
style="position: absolute; top: 0px; left: 0px; width: 100%; height: 100%; object-fit: cover; object-position: center; opacity: 0; transition: opacity 1000ms;"
@@ -68,6 +69,7 @@ exports[` should render fixed size images 1`] = `
crossorigin="anonymous"
height="100"
itemprop="item-prop-for-the-image"
+ loading="lazy"
src="test_image.jpg"
srcset="some srcSet"
style="position: absolute; top: 0px; left: 0px; width: 100%; height: 100%; object-fit: cover; object-position: center; opacity: 0; transition: opacity 500ms;"
@@ -112,6 +114,7 @@ exports[` should render fluid images 1`] = `
alt="Alt text for the image"
crossorigin="anonymous"
itemprop="item-prop-for-the-image"
+ loading="lazy"
sizes="(max-width: 600px) 100vw, 600px"
src="test_image.jpg"
srcset="some srcSet"
diff --git a/packages/gatsby-image/src/__tests__/index.js b/packages/gatsby-image/src/__tests__/index.js
index 2085ba0218c75..102963e2c686b 100644
--- a/packages/gatsby-image/src/__tests__/index.js
+++ b/packages/gatsby-image/src/__tests__/index.js
@@ -69,6 +69,7 @@ describe(``, () => {
expect(imageTag.getAttribute(`title`)).toEqual(`Title for the image`)
expect(imageTag.getAttribute(`alt`)).toEqual(`Alt text for the image`)
expect(imageTag.getAttribute(`crossOrigin`)).toEqual(`anonymous`)
+ expect(imageTag.getAttribute(`loading`)).toEqual(`lazy`)
})
it(`should have correct placeholder src, title, style and class attributes`, () => {
@@ -85,10 +86,16 @@ describe(``, () => {
})
it(`should have a transition-delay of 1sec`, () => {
- const component = setup(false, { durationFadeIn: `1000` })
+ const component = setup(false, { durationFadeIn: 1000 })
expect(component).toMatchSnapshot()
})
+ it(`should have the the "critical" prop set "loading='eager'"`, () => {
+ const props = { critical: true }
+ const imageTag = setup(false, props).querySelector(`picture img`)
+ expect(imageTag.getAttribute(`loading`)).toEqual(`eager`)
+ })
+
it(`should call onLoad and onError image events`, () => {
const onLoadMock = jest.fn()
const onErrorMock = jest.fn()
diff --git a/packages/gatsby-image/src/index.js b/packages/gatsby-image/src/index.js
index feba030e367e0..80c9d440fa894 100644
--- a/packages/gatsby-image/src/index.js
+++ b/packages/gatsby-image/src/index.js
@@ -1,18 +1,42 @@
import React from "react"
import PropTypes from "prop-types"
-// Handle legacy names for image queries.
+const logDeprecationNotice = (prop, replacement) => {
+ if (process.env.NODE_ENV === `production`) {
+ return
+ }
+
+ console.log(
+ `
+ The "${prop}" prop is now deprecated and will be removed in the next major version
+ of "gatsby-image".
+ `
+ )
+
+ if (replacement) {
+ console.log(`Please use ${replacement} instead of "${prop}".`)
+ }
+}
+
+// Handle legacy props during their deprecation phase
const convertProps = props => {
let convertedProps = { ...props }
- if (convertedProps.resolutions) {
- convertedProps.fixed = convertedProps.resolutions
+ const { resolutions, sizes, critical } = convertedProps
+
+ if (resolutions) {
+ convertedProps.fixed = resolutions
delete convertedProps.resolutions
}
- if (convertedProps.sizes) {
- convertedProps.fluid = convertedProps.sizes
+ if (sizes) {
+ convertedProps.fluid = sizes
delete convertedProps.sizes
}
+ if (critical) {
+ logDeprecationNotice(`critical`, `the native "loading" attribute`)
+ convertedProps.loading = `eager`
+ }
+
return convertedProps
}
@@ -39,6 +63,14 @@ const activateCacheForImage = props => {
imageCache[src] = true
}
+// Native lazy-loading support: https://addyosmani.com/blog/lazy-loading/
+const hasNativeLazyLoadSupport =
+ typeof HTMLImageElement !== `undefined` &&
+ `loading` in HTMLImageElement.prototype
+
+const isBrowser = typeof window !== `undefined`
+const hasIOSupport = isBrowser && window.IntersectionObserver
+
let io
const listeners = new WeakMap()
@@ -99,12 +131,6 @@ const noscriptImg = props => {
const crossOrigin = props.crossOrigin
? `crossorigin="${props.crossOrigin}" `
: ``
-
- // Since we're in the noscript block for this image (which is rendered during SSR or when js is disabled),
- // we have no way to "detect" if native lazy loading is supported by the user's browser
- // Since this attribute is a progressive enhancement, it won't break a browser with no support
- // Therefore setting it by default is a good idea.
-
const loading = props.loading ? `loading="${props.loading}" ` : ``
return ``
@@ -118,17 +144,10 @@ const Img = React.forwardRef((props, ref) => {
style,
onLoad,
onError,
- nativeLazyLoadSupported,
loading,
...otherProps
} = props
- let loadingAttribute = {}
-
- if (nativeLazyLoadSupported) {
- loadingAttribute.loading = loading
- }
-
return (
{
onLoad={onLoad}
onError={onError}
ref={ref}
- {...loadingAttribute}
+ loading={loading}
style={{
position: `absolute`,
top: 0,
@@ -163,61 +182,26 @@ class Image extends React.Component {
constructor(props) {
super(props)
- // default settings for browser without Intersection Observer available
- let isVisible = true
- let imgLoaded = false
- let imgCached = false
- let IOSupported = false
- let fadeIn = props.fadeIn
- let nativeLazyLoadSupported = false
-
// If this image has already been loaded before then we can assume it's
// already in the browser cache so it's cheap to just show directly.
- const seenBefore = inImageCache(props)
-
- // browser with Intersection Observer available
- if (
- !seenBefore &&
- typeof window !== `undefined` &&
- window.IntersectionObserver
- ) {
- isVisible = false
- IOSupported = true
- }
-
- // Chrome Canary 75 added native lazy loading support!
- // https://addyosmani.com/blog/lazy-loading/
- if (
- typeof HTMLImageElement !== `undefined` &&
- `loading` in HTMLImageElement.prototype
- ) {
- // Setting isVisible to true to short circuit our IO code and let the browser do its magic
- isVisible = true
- nativeLazyLoadSupported = true
- }
-
- // Never render image during SSR
- if (typeof window === `undefined`) {
- isVisible = false
- }
+ this.seenBefore = isBrowser && inImageCache(props)
- // Force render for critical images
- if (props.critical) {
- isVisible = true
- IOSupported = false
- }
+ this.addNoScript = !(props.critical && !props.fadeIn)
+ this.useIOSupport =
+ !hasNativeLazyLoadSupport &&
+ hasIOSupport &&
+ !props.critical &&
+ !this.seenBefore
- const hasNoScript = !(props.critical && !props.fadeIn)
+ const isVisible =
+ props.critical ||
+ (isBrowser && (hasNativeLazyLoadSupport || !this.useIOSupport))
this.state = {
isVisible,
- imgLoaded,
- imgCached,
- IOSupported,
- fadeIn,
- hasNoScript,
- seenBefore,
- nativeLazyLoadSupported,
+ imgLoaded: false,
+ imgCached: false,
+ fadeIn: !this.seenBefore && props.fadeIn,
}
this.imageRef = React.createRef()
@@ -243,12 +227,9 @@ class Image extends React.Component {
}
}
+ // Specific to IntersectionObserver based lazy-load support
handleRef(ref) {
- if (this.state.nativeLazyLoadSupported) {
- // Bail because the browser natively supports lazy loading
- return
- }
- if (this.state.IOSupported && ref) {
+ if (this.useIOSupport && ref) {
this.cleanUpListeners = listenToIntersections(ref, () => {
const imageInCache = inImageCache(this.props)
if (
@@ -265,6 +246,8 @@ class Image extends React.Component {
this.setState({ isVisible: true }, () =>
this.setState({
imgLoaded: imageInCache,
+ // `currentSrc` should be a string, but can be `undefined` in IE,
+ // !! operator validates the value is not undefined/null/""
imgCached: !!this.imageRef.current.currentSrc,
})
)
@@ -276,9 +259,6 @@ class Image extends React.Component {
activateCacheForImage(this.props)
this.setState({ imgLoaded: true })
- if (this.state.seenBefore) {
- this.setState({ fadeIn: false })
- }
if (this.props.onLoad) {
this.props.onLoad()
@@ -300,31 +280,10 @@ class Image extends React.Component {
durationFadeIn,
Tag,
itemProp,
- critical,
+ loading,
} = convertProps(this.props)
- let { loading } = convertProps(this.props)
-
- if (
- typeof critical === `boolean` &&
- process.env.NODE_ENV !== `production`
- ) {
- console.log(
- `
- The "critical" prop is now deprecated and will be removed in the next major version
- of "gatsby-image"
-
- Please use the native "loading" attribute instead of "critical"
- `
- )
- // We want to continue supporting critical and in case it is passed in
- // we map its value to loading
- loading = critical ? `eager` : `lazy`
- }
-
- const { nativeLazyLoadSupported } = this.state
-
- const shouldReveal = this.state.imgLoaded || this.state.fadeIn === false
+ const shouldReveal = this.state.fadeIn === false || this.state.imgLoaded
const shouldFadeIn = this.state.fadeIn === true && !this.state.imgCached
const imageStyle = {
@@ -426,14 +385,13 @@ class Image extends React.Component {
onLoad={this.handleImageLoaded}
onError={this.props.onError}
itemProp={itemProp}
- nativeLazyLoadSupported={nativeLazyLoadSupported}
loading={loading}
/>
)}
{/* Show the original image during server-side rendering if JavaScript is disabled */}
- {this.state.hasNoScript && (
+ {this.addNoScript && (
)}
{/* Show the original image during server-side rendering if JavaScript is disabled */}
- {this.state.hasNoScript && (
+ {this.addNoScript && (