Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(gatsby-image): Add support for native lazy loading #13217

Merged
merged 15 commits into from
May 16, 2019
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions packages/gatsby-image/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -352,10 +352,16 @@ 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`. |
| `objectFit` | `string` | Passed to the `object-fit-images` polyfill when importing from `gatsby-image/withIEPolyfill`. \ |
| `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`. |
sidharthachatterjee marked this conversation as resolved.
Show resolved Hide resolved
| |
| `critical` | `bool` | Opt-out of lazy-loading behavior. Defaults to `false`. Deprecated, use `loading` instead. |
| Defaults to `cover`. |

## Image processing arguments

[gatsby-plugin-sharp](/packages/gatsby-plugin-sharp) supports many additional arguments for transforming your images like
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 @@ -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
86 changes: 82 additions & 4 deletions packages/gatsby-image/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,11 +100,36 @@ 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}" ` : ``
sidharthachatterjee marked this conversation as resolved.
Show resolved Hide resolved

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
}

console.log(props)

return (
<img
Expand All @@ -115,6 +140,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 +171,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 +187,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` &&
sidharthachatterjee marked this conversation as resolved.
Show resolved Hide resolved
`loading` in HTMLImageElement.prototype
) {
// Setting isVisible to true to short circuit our IO code and let the browser do its magic
isVisible = true
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe to make it clear this is now representing two different states it could be changed to isVisibleOrNativeLazyLoadingSupported — verbose is effective :-D

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@KyleAMathews isn't the isVisible state here linked to the image components visibility state for it's picture element? I suggested a refactor here, but perhaps that's the wrong approach.

nativeLazyLoadSupported = true
}

// Never render image during SSR
if (typeof window === `undefined`) {
isVisible = false
Expand All @@ -181,6 +219,7 @@ class Image extends React.Component {
fadeIn,
hasNoScript,
seenBefore,
nativeLazyLoadSupported,
}

this.imageRef = React.createRef()
Expand All @@ -207,6 +246,10 @@ class Image extends React.Component {
}

handleRef(ref) {
if (this.state.nativeLazyLoadSupported) {
// Bail because the browser natively supports lazy loading
return
}
sidharthachatterjee marked this conversation as resolved.
Show resolved Hide resolved
if (this.state.IOSupported && ref) {
this.cleanUpListeners = listenToIntersections(ref, () => {
const imageInCache = inImageCache(this.props)
Expand Down Expand Up @@ -259,8 +302,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 +428,8 @@ class Image extends React.Component {
onLoad={this.handleImageLoaded}
onError={this.props.onError}
itemProp={itemProp}
nativeLazyLoadSupported={nativeLazyLoadSupported}
loading={loading}
/>
</picture>
)}
Expand All @@ -371,7 +438,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 +522,8 @@ class Image extends React.Component {
onLoad={this.handleImageLoaded}
onError={this.props.onError}
itemProp={itemProp}
nativeLazyLoadSupported={nativeLazyLoadSupported}
loading={loading}
/>
</picture>
)}
Expand All @@ -461,6 +535,7 @@ class Image extends React.Component {
__html: noscriptImg({
alt,
title,
loading,
...image,
}),
}}
Expand All @@ -475,11 +550,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`,
sidharthachatterjee marked this conversation as resolved.
Show resolved Hide resolved
}

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

export default Image