diff --git a/packages/gatsby-image/README.md b/packages/gatsby-image/README.md index 5078188cb448b..d34a5d726ceb9 100644 --- a/packages/gatsby-image/README.md +++ b/packages/gatsby-image/README.md @@ -352,9 +352,10 @@ You will need to add it in your graphql query as is shown in the following snipp | `onStartLoad` | `func` | A callback that is called when the full-size image starts loading, it gets the parameter { wasCached: boolean } provided. | | `onError` | `func` | A callback that is called when the image fails to load. | | `Tag` | `string` | Which HTML tag to use for wrapping elements. Defaults to `div`. | -| `critical` | `bool` | Opt-out of lazy-loading behavior. Defaults to `false`. | | `objectFit` | `string` | Passed to the `object-fit-images` polyfill when importing from `gatsby-image/withIEPolyfill`. Defaults to `cover`. | | `objectPosition` | `string` | Passed to the `object-fit-images` polyfill when importing from `gatsby-image/withIEPolyfill`. Defaults to `50% 50%`. | +| `loading` | `string` | Set the browser's native lazy loading attribute. One of `lazy`, `eager` or `auto`. Defaults to `lazy`. | +| `critical` | `bool` | Opt-out of lazy-loading behavior. Defaults to `false`. Deprecated, use `loading` instead. | ## Image processing arguments diff --git a/packages/gatsby-image/index.d.ts b/packages/gatsby-image/index.d.ts index 910ba9ef8059c..bbc4f2fe8d9d2 100644 --- a/packages/gatsby-image/index.d.ts +++ b/packages/gatsby-image/index.d.ts @@ -42,6 +42,7 @@ interface GatsbyImageProps { onError?: (event: any) => void Tag?: string itemProp?: string + loading?: `auto` | `lazy` | `eager` } export default class GatsbyImage extends React.Component< diff --git a/packages/gatsby-image/src/__tests__/__snapshots__/index.js.snap b/packages/gatsby-image/src/__tests__/__snapshots__/index.js.snap index 892ff4ffca341..53c1acf582abc 100644 --- a/packages/gatsby-image/src/__tests__/__snapshots__/index.js.snap +++ b/packages/gatsby-image/src/__tests__/__snapshots__/index.js.snap @@ -35,7 +35,7 @@ exports[` should have a transition-delay of 1sec 1`] = ` /> @@ -76,7 +76,7 @@ exports[` should render fixed size images 1`] = ` /> @@ -120,7 +120,7 @@ exports[` should render fluid images 1`] = ` /> diff --git a/packages/gatsby-image/src/index.js b/packages/gatsby-image/src/index.js index 84ae8f40aee85..feba030e367e0 100644 --- a/packages/gatsby-image/src/index.js +++ b/packages/gatsby-image/src/index.js @@ -100,11 +100,34 @@ const noscriptImg = props => { ? `crossorigin="${props.crossOrigin}" ` : `` - return `${srcSetWebp}` + // 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 `${srcSetWebp}` } const Img = React.forwardRef((props, ref) => { - const { sizes, srcSet, src, style, onLoad, onError, ...otherProps } = props + const { + sizes, + srcSet, + src, + style, + onLoad, + onError, + nativeLazyLoadSupported, + loading, + ...otherProps + } = props + + let loadingAttribute = {} + + if (nativeLazyLoadSupported) { + loadingAttribute.loading = loading + } return ( { onLoad={onLoad} onError={onError} ref={ref} + {...loadingAttribute} style={{ position: `absolute`, top: 0, @@ -145,6 +169,7 @@ class Image extends React.Component { 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. @@ -160,6 +185,17 @@ class Image extends React.Component { 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 @@ -181,6 +217,7 @@ class Image extends React.Component { fadeIn, hasNoScript, seenBefore, + nativeLazyLoadSupported, } this.imageRef = React.createRef() @@ -207,6 +244,10 @@ class Image extends React.Component { } handleRef(ref) { + if (this.state.nativeLazyLoadSupported) { + // Bail because the browser natively supports lazy loading + return + } if (this.state.IOSupported && ref) { this.cleanUpListeners = listenToIntersections(ref, () => { const imageInCache = inImageCache(this.props) @@ -259,8 +300,30 @@ class Image extends React.Component { durationFadeIn, Tag, itemProp, + critical, } = 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 shouldFadeIn = this.state.fadeIn === true && !this.state.imgCached @@ -363,6 +426,8 @@ class Image extends React.Component { onLoad={this.handleImageLoaded} onError={this.props.onError} itemProp={itemProp} + nativeLazyLoadSupported={nativeLazyLoadSupported} + loading={loading} /> )} @@ -371,7 +436,12 @@ class Image extends React.Component { {this.state.hasNoScript && (