Skip to content

Commit 3c0eb1e

Browse files
sidharthachatterjeeKyleAMathews
authored andcommitted
feat(gatsby-image): Add support for native lazy loading (#13217)
* 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 <DSchau@users.noreply.github.com> * chore: format * Do not show deprecation message in production * Clean up markdown table * Clean up markdown table again * Fix test
1 parent e47da77 commit 3c0eb1e

File tree

4 files changed

+86
-8
lines changed

4 files changed

+86
-8
lines changed

packages/gatsby-image/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -352,9 +352,10 @@ You will need to add it in your graphql query as is shown in the following snipp
352352
| `onStartLoad` | `func` | A callback that is called when the full-size image starts loading, it gets the parameter { wasCached: boolean } provided. |
353353
| `onError` | `func` | A callback that is called when the image fails to load. |
354354
| `Tag` | `string` | Which HTML tag to use for wrapping elements. Defaults to `div`. |
355-
| `critical` | `bool` | Opt-out of lazy-loading behavior. Defaults to `false`. |
356355
| `objectFit` | `string` | Passed to the `object-fit-images` polyfill when importing from `gatsby-image/withIEPolyfill`. Defaults to `cover`. |
357356
| `objectPosition` | `string` | Passed to the `object-fit-images` polyfill when importing from `gatsby-image/withIEPolyfill`. Defaults to `50% 50%`. |
357+
| `loading` | `string` | Set the browser's native lazy loading attribute. One of `lazy`, `eager` or `auto`. Defaults to `lazy`. |
358+
| `critical` | `bool` | Opt-out of lazy-loading behavior. Defaults to `false`. Deprecated, use `loading` instead. |
358359

359360
## Image processing arguments
360361

packages/gatsby-image/index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ interface GatsbyImageProps {
4242
onError?: (event: any) => void
4343
Tag?: string
4444
itemProp?: string
45+
loading?: `auto` | `lazy` | `eager`
4546
}
4647

4748
export default class GatsbyImage extends React.Component<

packages/gatsby-image/src/__tests__/__snapshots__/index.js.snap

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ exports[`<Image /> should have a transition-delay of 1sec 1`] = `
3535
/>
3636
</picture>
3737
<noscript>
38-
&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;
38+
&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;
3939
</noscript>
4040
</div>
4141
</div>
@@ -76,7 +76,7 @@ exports[`<Image /> should render fixed size images 1`] = `
7676
/>
7777
</picture>
7878
<noscript>
79-
&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;
79+
&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;
8080
</noscript>
8181
</div>
8282
</div>
@@ -120,7 +120,7 @@ exports[`<Image /> should render fluid images 1`] = `
120120
/>
121121
</picture>
122122
<noscript>
123-
&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;
123+
&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;
124124
</noscript>
125125
</div>
126126
</div>

packages/gatsby-image/src/index.js

Lines changed: 80 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -100,11 +100,34 @@ const noscriptImg = props => {
100100
? `crossorigin="${props.crossOrigin}" `
101101
: ``
102102

103-
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>`
103+
// Since we're in the noscript block for this image (which is rendered during SSR or when js is disabled),
104+
// we have no way to "detect" if native lazy loading is supported by the user's browser
105+
// Since this attribute is a progressive enhancement, it won't break a browser with no support
106+
// Therefore setting it by default is a good idea.
107+
108+
const loading = props.loading ? `loading="${props.loading}" ` : ``
109+
110+
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>`
104111
}
105112

106113
const Img = React.forwardRef((props, ref) => {
107-
const { sizes, srcSet, src, style, onLoad, onError, ...otherProps } = props
114+
const {
115+
sizes,
116+
srcSet,
117+
src,
118+
style,
119+
onLoad,
120+
onError,
121+
nativeLazyLoadSupported,
122+
loading,
123+
...otherProps
124+
} = props
125+
126+
let loadingAttribute = {}
127+
128+
if (nativeLazyLoadSupported) {
129+
loadingAttribute.loading = loading
130+
}
108131

109132
return (
110133
<img
@@ -115,6 +138,7 @@ const Img = React.forwardRef((props, ref) => {
115138
onLoad={onLoad}
116139
onError={onError}
117140
ref={ref}
141+
{...loadingAttribute}
118142
style={{
119143
position: `absolute`,
120144
top: 0,
@@ -145,6 +169,7 @@ class Image extends React.Component {
145169
let imgCached = false
146170
let IOSupported = false
147171
let fadeIn = props.fadeIn
172+
let nativeLazyLoadSupported = false
148173

149174
// If this image has already been loaded before then we can assume it's
150175
// already in the browser cache so it's cheap to just show directly.
@@ -160,6 +185,17 @@ class Image extends React.Component {
160185
IOSupported = true
161186
}
162187

188+
// Chrome Canary 75 added native lazy loading support!
189+
// https://addyosmani.com/blog/lazy-loading/
190+
if (
191+
typeof HTMLImageElement !== `undefined` &&
192+
`loading` in HTMLImageElement.prototype
193+
) {
194+
// Setting isVisible to true to short circuit our IO code and let the browser do its magic
195+
isVisible = true
196+
nativeLazyLoadSupported = true
197+
}
198+
163199
// Never render image during SSR
164200
if (typeof window === `undefined`) {
165201
isVisible = false
@@ -181,6 +217,7 @@ class Image extends React.Component {
181217
fadeIn,
182218
hasNoScript,
183219
seenBefore,
220+
nativeLazyLoadSupported,
184221
}
185222

186223
this.imageRef = React.createRef()
@@ -207,6 +244,10 @@ class Image extends React.Component {
207244
}
208245

209246
handleRef(ref) {
247+
if (this.state.nativeLazyLoadSupported) {
248+
// Bail because the browser natively supports lazy loading
249+
return
250+
}
210251
if (this.state.IOSupported && ref) {
211252
this.cleanUpListeners = listenToIntersections(ref, () => {
212253
const imageInCache = inImageCache(this.props)
@@ -259,8 +300,30 @@ class Image extends React.Component {
259300
durationFadeIn,
260301
Tag,
261302
itemProp,
303+
critical,
262304
} = convertProps(this.props)
263305

306+
let { loading } = convertProps(this.props)
307+
308+
if (
309+
typeof critical === `boolean` &&
310+
process.env.NODE_ENV !== `production`
311+
) {
312+
console.log(
313+
`
314+
The "critical" prop is now deprecated and will be removed in the next major version
315+
of "gatsby-image"
316+
317+
Please use the native "loading" attribute instead of "critical"
318+
`
319+
)
320+
// We want to continue supporting critical and in case it is passed in
321+
// we map its value to loading
322+
loading = critical ? `eager` : `lazy`
323+
}
324+
325+
const { nativeLazyLoadSupported } = this.state
326+
264327
const shouldReveal = this.state.imgLoaded || this.state.fadeIn === false
265328
const shouldFadeIn = this.state.fadeIn === true && !this.state.imgCached
266329

@@ -363,6 +426,8 @@ class Image extends React.Component {
363426
onLoad={this.handleImageLoaded}
364427
onError={this.props.onError}
365428
itemProp={itemProp}
429+
nativeLazyLoadSupported={nativeLazyLoadSupported}
430+
loading={loading}
366431
/>
367432
</picture>
368433
)}
@@ -371,7 +436,12 @@ class Image extends React.Component {
371436
{this.state.hasNoScript && (
372437
<noscript
373438
dangerouslySetInnerHTML={{
374-
__html: noscriptImg({ alt, title, ...image }),
439+
__html: noscriptImg({
440+
alt,
441+
title,
442+
loading,
443+
...image,
444+
}),
375445
}}
376446
/>
377447
)}
@@ -450,6 +520,8 @@ class Image extends React.Component {
450520
onLoad={this.handleImageLoaded}
451521
onError={this.props.onError}
452522
itemProp={itemProp}
523+
nativeLazyLoadSupported={nativeLazyLoadSupported}
524+
loading={loading}
453525
/>
454526
</picture>
455527
)}
@@ -461,6 +533,7 @@ class Image extends React.Component {
461533
__html: noscriptImg({
462534
alt,
463535
title,
536+
loading,
464537
...image,
465538
}),
466539
}}
@@ -475,11 +548,13 @@ class Image extends React.Component {
475548
}
476549

477550
Image.defaultProps = {
478-
critical: false,
479551
fadeIn: true,
480552
durationFadeIn: 500,
481553
alt: ``,
482554
Tag: `div`,
555+
// We set it to `lazy` by default because it's best to default to a performant
556+
// setting and let the user "opt out" to `eager`
557+
loading: `lazy`,
483558
}
484559

485560
const fixedObject = PropTypes.shape({
@@ -526,6 +601,7 @@ Image.propTypes = {
526601
onStartLoad: PropTypes.func,
527602
Tag: PropTypes.string,
528603
itemProp: PropTypes.string,
604+
loading: PropTypes.oneOf([`auto`, `lazy`, `eager`]),
529605
}
530606

531607
export default Image

0 commit comments

Comments
 (0)