-
Notifications
You must be signed in to change notification settings - Fork 10.2k
feat(gatsby-image): Add support for native lazy loading #13217
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
Changes from all commits
ccfd881
186bf69
617c58f
419d5e2
7f15270
7cca14b
8800618
73a13d1
2d3116b
62ff08d
8371773
3a1f53e
585d51f
f774b87
182f14b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
|
@@ -115,6 +138,7 @@ const Img = React.forwardRef((props, ref) => { | |
| 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` && | ||
|
sidharthachatterjee marked this conversation as resolved.
|
||
| `loading` in HTMLImageElement.prototype | ||
| ) { | ||
| // Setting isVisible to true to short circuit our IO code and let the browser do its magic | ||
| isVisible = true | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @KyleAMathews isn't the |
||
| 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 | ||
| } | ||
|
sidharthachatterjee marked this conversation as resolved.
|
||
| 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} | ||
| /> | ||
| </picture> | ||
| )} | ||
|
|
@@ -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, | ||
| }), | ||
| }} | ||
| /> | ||
| )} | ||
|
|
@@ -450,6 +520,8 @@ class Image extends React.Component { | |
| onLoad={this.handleImageLoaded} | ||
| onError={this.props.onError} | ||
| itemProp={itemProp} | ||
| nativeLazyLoadSupported={nativeLazyLoadSupported} | ||
| loading={loading} | ||
| /> | ||
| </picture> | ||
| )} | ||
|
|
@@ -461,6 +533,7 @@ class Image extends React.Component { | |
| __html: noscriptImg({ | ||
| alt, | ||
| title, | ||
| loading, | ||
| ...image, | ||
| }), | ||
| }} | ||
|
|
@@ -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`, | ||
|
sidharthachatterjee marked this conversation as resolved.
|
||
| } | ||
|
|
||
| const fixedObject = PropTypes.shape({ | ||
|
|
@@ -526,6 +601,7 @@ Image.propTypes = { | |
| onStartLoad: PropTypes.func, | ||
| Tag: PropTypes.string, | ||
| itemProp: PropTypes.string, | ||
| loading: PropTypes.oneOf([`auto`, `lazy`, `eager`]), | ||
| } | ||
|
|
||
| export default Image | ||
Uh oh!
There was an error while loading. Please reload this page.