Skip to content

Commit

Permalink
feat(gatsby-image): Add support for native lazy loading (gatsbyjs#13217)
Browse files Browse the repository at this point in the history
* Add support for new native lazy loading to gatsby-image

* Add loading prop to typings

* Fix feature check

* Fix optional prop

* Update snapshots

* Deprecate critical and map its value to loading

* Document new loading attribute

* Update comment

* Apply suggestions from code review

Co-Authored-By: Dustin Schau <[email protected]>

* chore: format

* Do not show deprecation message in production

* Clean up markdown table

* Clean up markdown table again

* Fix test
  • Loading branch information
sidharthachatterjee authored and KyleAMathews committed May 16, 2019
1 parent e47da77 commit 3c0eb1e
Show file tree
Hide file tree
Showing 4 changed files with 86 additions and 8 deletions.
3 changes: 2 additions & 1 deletion packages/gatsby-image/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions packages/gatsby-image/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ exports[`<Image /> should have a transition-delay of 1sec 1`] = `
/>
</picture>
<noscript>
&lt;picture&gt;&lt;source type='image/webp' srcset="some srcSetWebp" /&gt;&lt;img width="100" height="100" srcset="some srcSet" src="test_image.jpg" alt="Alt text for the image" title="Title for the image" style="position:absolute;top:0;left:0;opacity:1;width:100%;height:100%;object-fit:cover;object-position:center"/&gt;&lt;/picture&gt;
&lt;picture&gt;&lt;source type='image/webp' srcset="some srcSetWebp" /&gt;&lt;img loading="lazy" width="100" height="100" srcset="some srcSet" src="test_image.jpg" alt="Alt text for the image" title="Title for the image" style="position:absolute;top:0;left:0;opacity:1;width:100%;height:100%;object-fit:cover;object-position:center"/&gt;&lt;/picture&gt;
</noscript>
</div>
</div>
Expand Down Expand Up @@ -76,7 +76,7 @@ exports[`<Image /> should render fixed size images 1`] = `
/>
</picture>
<noscript>
&lt;picture&gt;&lt;source type='image/webp' srcset="some srcSetWebp" /&gt;&lt;img width="100" height="100" srcset="some srcSet" src="test_image.jpg" alt="Alt text for the image" title="Title for the image" style="position:absolute;top:0;left:0;opacity:1;width:100%;height:100%;object-fit:cover;object-position:center"/&gt;&lt;/picture&gt;
&lt;picture&gt;&lt;source type='image/webp' srcset="some srcSetWebp" /&gt;&lt;img loading="lazy" width="100" height="100" srcset="some srcSet" src="test_image.jpg" alt="Alt text for the image" title="Title for the image" style="position:absolute;top:0;left:0;opacity:1;width:100%;height:100%;object-fit:cover;object-position:center"/&gt;&lt;/picture&gt;
</noscript>
</div>
</div>
Expand Down Expand Up @@ -120,7 +120,7 @@ exports[`<Image /> should render fluid images 1`] = `
/>
</picture>
<noscript>
&lt;picture&gt;&lt;source type='image/webp' srcset="some srcSetWebp" sizes="(max-width: 600px) 100vw, 600px" /&gt;&lt;img sizes="(max-width: 600px) 100vw, 600px" srcset="some srcSet" src="test_image.jpg" alt="Alt text for the image" title="Title for the image" style="position:absolute;top:0;left:0;opacity:1;width:100%;height:100%;object-fit:cover;object-position:center"/&gt;&lt;/picture&gt;
&lt;picture&gt;&lt;source type='image/webp' srcset="some srcSetWebp" sizes="(max-width: 600px) 100vw, 600px" /&gt;&lt;img loading="lazy" sizes="(max-width: 600px) 100vw, 600px" srcset="some srcSet" src="test_image.jpg" alt="Alt text for the image" title="Title for the image" style="position:absolute;top:0;left:0;opacity:1;width:100%;height:100%;object-fit:cover;object-position:center"/&gt;&lt;/picture&gt;
</noscript>
</div>
</div>
Expand Down
84 changes: 80 additions & 4 deletions packages/gatsby-image/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,11 +100,34 @@ const noscriptImg = props => {
? `crossorigin="${props.crossOrigin}" `
: ``

return `<picture>${srcSetWebp}<img ${width}${height}${sizes}${srcSet}${src}${alt}${title}${crossOrigin}style="position:absolute;top:0;left:0;opacity:1;width:100%;height:100%;object-fit:cover;object-position:center"/></picture>`
// 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 `<picture>${srcSetWebp}<img ${loading}${width}${height}${sizes}${srcSet}${src}${alt}${title}${crossOrigin}style="position:absolute;top:0;left:0;opacity:1;width:100%;height:100%;object-fit:cover;object-position:center"/></picture>`
}

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 (
<img
Expand All @@ -115,6 +138,7 @@ const Img = React.forwardRef((props, ref) => {
onLoad={onLoad}
onError={onError}
ref={ref}
{...loadingAttribute}
style={{
position: `absolute`,
top: 0,
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -181,6 +217,7 @@ class Image extends React.Component {
fadeIn,
hasNoScript,
seenBefore,
nativeLazyLoadSupported,
}

this.imageRef = React.createRef()
Expand All @@ -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)
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -363,6 +426,8 @@ class Image extends React.Component {
onLoad={this.handleImageLoaded}
onError={this.props.onError}
itemProp={itemProp}
nativeLazyLoadSupported={nativeLazyLoadSupported}
loading={loading}
/>
</picture>
)}
Expand All @@ -371,7 +436,12 @@ class Image extends React.Component {
{this.state.hasNoScript && (
<noscript
dangerouslySetInnerHTML={{
__html: noscriptImg({ alt, title, ...image }),
__html: noscriptImg({
alt,
title,
loading,
...image,
}),
}}
/>
)}
Expand Down Expand Up @@ -450,6 +520,8 @@ class Image extends React.Component {
onLoad={this.handleImageLoaded}
onError={this.props.onError}
itemProp={itemProp}
nativeLazyLoadSupported={nativeLazyLoadSupported}
loading={loading}
/>
</picture>
)}
Expand All @@ -461,6 +533,7 @@ class Image extends React.Component {
__html: noscriptImg({
alt,
title,
loading,
...image,
}),
}}
Expand All @@ -475,11 +548,13 @@ class Image extends React.Component {
}

Image.defaultProps = {
critical: false,
fadeIn: true,
durationFadeIn: 500,
alt: ``,
Tag: `div`,
// We set it to `lazy` by default because it's best to default to a performant
// setting and let the user "opt out" to `eager`
loading: `lazy`,
}

const fixedObject = PropTypes.shape({
Expand Down Expand Up @@ -526,6 +601,7 @@ Image.propTypes = {
onStartLoad: PropTypes.func,
Tag: PropTypes.string,
itemProp: PropTypes.string,
loading: PropTypes.oneOf([`auto`, `lazy`, `eager`]),
}

export default Image

0 comments on commit 3c0eb1e

Please sign in to comment.