Change Image component lazy=true to loading=lazy (#18138)

This PR updates the `<Image>` component to follow the same property naming as native `<img>`.

https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Img#attr-loading

This currently allows two values,`loading=lazy` and `loading=eager`, but there might be new values added in a future spec.

cc @atcastle
This commit is contained in:
Steven 2020-10-22 14:59:42 -04:00 committed by GitHub
parent fe4d16c7f4
commit 07adc8ef26
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 82 additions and 20 deletions

View file

@ -1,6 +1,9 @@
import React, { ReactElement, useEffect, useRef } from 'react' import React, { ReactElement, useEffect, useRef } from 'react'
import Head from '../next-server/lib/head' import Head from '../next-server/lib/head'
const VALID_LOADING_VALUES = ['lazy', 'eager', undefined] as const
type LoadingValue = typeof VALID_LOADING_VALUES[number]
const loaders = new Map<LoaderKey, (props: LoaderProps) => string>([ const loaders = new Map<LoaderKey, (props: LoaderProps) => string>([
['imgix', imgixLoader], ['imgix', imgixLoader],
['cloudinary', cloudinaryLoader], ['cloudinary', cloudinaryLoader],
@ -18,12 +21,12 @@ type ImageData = {
type ImageProps = Omit< type ImageProps = Omit<
JSX.IntrinsicElements['img'], JSX.IntrinsicElements['img'],
'src' | 'srcSet' | 'ref' | 'width' | 'height' 'src' | 'srcSet' | 'ref' | 'width' | 'height' | 'loading'
> & { > & {
src: string src: string
quality?: string quality?: string
priority?: boolean priority?: boolean
lazy?: boolean loading?: LoadingValue
unoptimized?: boolean unoptimized?: boolean
} & ( } & (
| { width: number; height: number; unsized?: false } | { width: number; height: number; unsized?: false }
@ -142,7 +145,7 @@ export default function Image({
sizes, sizes,
unoptimized = false, unoptimized = false,
priority = false, priority = false,
lazy, loading,
className, className,
quality, quality,
width, width,
@ -152,17 +155,23 @@ export default function Image({
}: ImageProps) { }: ImageProps) {
const thisEl = useRef<HTMLImageElement>(null) const thisEl = useRef<HTMLImageElement>(null)
// Sanity Checks: if (process.env.NODE_ENV !== 'production') {
// If priority and lazy are present, log an error and use priority only. if (!VALID_LOADING_VALUES.includes(loading)) {
if (priority && lazy) {
if (process.env.NODE_ENV !== 'production') {
throw new Error( throw new Error(
`Image with src "${src}" has both "priority" and "lazy" properties. Only one should be used.` `Image with src "${src}" has invalid "loading" property. Provided "${loading}" should be one of ${VALID_LOADING_VALUES.map(
String
).join(',')}.`
)
}
if (priority && loading === 'lazy') {
throw new Error(
`Image with src "${src}" has both "priority" and "loading=lazy" properties. Only one should be used.`
) )
} }
} }
if (!priority && typeof lazy === 'undefined') { let lazy = loading === 'lazy'
if (!priority && typeof loading === 'undefined') {
lazy = true lazy = true
} }

View file

@ -9,7 +9,7 @@ const ClientSide = () => {
<Image <Image
id="basic-image" id="basic-image"
src="foo.jpg" src="foo.jpg"
lazy={false} loading="eager"
width={300} width={300}
height={400} height={400}
quality={60} quality={60}
@ -18,7 +18,7 @@ const ClientSide = () => {
id="attribute-test" id="attribute-test"
data-demo="demo-value" data-demo="demo-value"
src="bar.jpg" src="bar.jpg"
lazy={false} loading="eager"
width={300} width={300}
height={400} height={400}
/> />
@ -27,7 +27,7 @@ const ClientSide = () => {
data-demo="demo-value" data-demo="demo-value"
host="secondary" host="secondary"
src="foo2.jpg" src="foo2.jpg"
lazy={false} loading="eager"
width={300} width={300}
height={400} height={400}
/> />
@ -35,7 +35,7 @@ const ClientSide = () => {
id="unoptimized-image" id="unoptimized-image"
unoptimized unoptimized
src="https://arbitraryurl.com/foo.jpg" src="https://arbitraryurl.com/foo.jpg"
lazy={false} loading="eager"
width={300} width={300}
height={400} height={400}
/> />

View file

@ -9,7 +9,7 @@ const Page = () => {
<Image <Image
id="basic-image" id="basic-image"
src="foo.jpg" src="foo.jpg"
lazy={false} loading="eager"
width={300} width={300}
height={400} height={400}
quality={60} quality={60}
@ -18,7 +18,7 @@ const Page = () => {
id="attribute-test" id="attribute-test"
data-demo="demo-value" data-demo="demo-value"
src="bar.jpg" src="bar.jpg"
lazy={false} loading="eager"
width={300} width={300}
height={400} height={400}
/> />
@ -27,7 +27,7 @@ const Page = () => {
data-demo="demo-value" data-demo="demo-value"
host="secondary" host="secondary"
src="foo2.jpg" src="foo2.jpg"
lazy={false} loading="eager"
width={300} width={300}
height={400} height={400}
/> />
@ -35,7 +35,7 @@ const Page = () => {
id="unoptimized-image" id="unoptimized-image"
unoptimized unoptimized
src="https://arbitraryurl.com/foo.jpg" src="https://arbitraryurl.com/foo.jpg"
lazy={false} loading="eager"
width={300} width={300}
height={400} height={400}
/> />

View file

@ -5,12 +5,18 @@ const Lazy = () => {
return ( return (
<div> <div>
<p id="stubtext">This is a page with lazy-loaded images</p> <p id="stubtext">This is a page with lazy-loaded images</p>
<Image id="lazy-top" src="foo1.jpg" height={400} width={300} lazy></Image> <Image
id="lazy-top"
src="foo1.jpg"
height={400}
width={300}
loading="lazy"
></Image>
<div style={{ height: '2000px' }}></div> <div style={{ height: '2000px' }}></div>
<Image <Image
id="lazy-mid" id="lazy-mid"
src="foo2.jpg" src="foo2.jpg"
lazy loading="lazy"
height={400} height={400}
width={300} width={300}
className="exampleclass" className="exampleclass"
@ -22,7 +28,22 @@ const Lazy = () => {
height={400} height={400}
width={300} width={300}
unoptimized unoptimized
lazy loading="lazy"
></Image>
<div style={{ height: '2000px' }}></div>
<Image
id="lazy-without-attribute"
src="foo4.jpg"
height={400}
width={300}
></Image>
<div style={{ height: '2000px' }}></div>
<Image
id="eager-loading"
src="foo5.jpg"
loading="eager"
height={400}
width={300}
></Image> ></Image>
</div> </div>
) )

View file

@ -132,6 +132,38 @@ function lazyLoadingTests() {
await browser.elementById('lazy-bottom').getAttribute('srcset') await browser.elementById('lazy-bottom').getAttribute('srcset')
).toBeFalsy() ).toBeFalsy()
}) })
it('should load the fourth image lazily after scrolling down', async () => {
expect(
await browser.elementById('lazy-without-attribute').getAttribute('src')
).toBeFalsy()
expect(
await browser.elementById('lazy-without-attribute').getAttribute('srcset')
).toBeFalsy()
let viewportHeight = await browser.eval(`window.innerHeight`)
let topOfBottomImage = await browser.eval(
`document.getElementById('lazy-without-attribute').parentElement.offsetTop`
)
let buffer = 150
await browser.eval(
`window.scrollTo(0, ${topOfBottomImage - (viewportHeight + buffer)})`
)
await waitFor(200)
expect(
await browser.elementById('lazy-without-attribute').getAttribute('src')
).toBe('https://example.com/myaccount/foo4.jpg?auto=format')
expect(
await browser.elementById('lazy-without-attribute').getAttribute('srcset')
).toBeTruthy()
})
it('should load the fifth image eagerly, without scrolling', async () => {
expect(await browser.elementById('eager-loading').getAttribute('src')).toBe(
'https://example.com/myaccount/foo5.jpg?auto=format'
)
expect(
await browser.elementById('eager-loading').getAttribute('srcset')
).toBeTruthy()
})
} }
async function hasPreloadLinkMatchingUrl(url) { async function hasPreloadLinkMatchingUrl(url) {