未验证 提交 c9eb3dca 编写于 作者: A Alex Castle 提交者: GitHub

Image component lazy loading (#17916)

Co-authored-by: NTim Neutkens <timneutkens@me.com>
Co-authored-by: NTim Neutkens <tim@timneutkens.nl>
上级 b37835ac
import React, { ReactElement } from 'react'
import React, { ReactElement, useEffect } from 'react'
import Head from '../next-server/lib/head'
const loaders: { [key: string]: (props: LoaderProps) => string } = {
......@@ -21,12 +21,49 @@ type ImageProps = Omit<JSX.IntrinsicElements['img'], 'src' | 'sizes'> & {
host?: string
sizes?: string
priority?: boolean
lazy: boolean
className: string
unoptimized?: boolean
}
let imageData: any = process.env.__NEXT_IMAGE_OPTS
const breakpoints = imageData.sizes || [640, 1024, 1600]
let cachedObserver: IntersectionObserver
const IntersectionObserver =
typeof window !== 'undefined' ? window.IntersectionObserver : null
function getObserver(): IntersectionObserver | undefined {
// Return shared instance of IntersectionObserver if already created
if (cachedObserver) {
return cachedObserver
}
// Only create shared IntersectionObserver if supported in browser
if (!IntersectionObserver) {
return undefined
}
return (cachedObserver = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
let lazyImage = entry.target as HTMLImageElement
if (lazyImage.dataset.src) {
lazyImage.src = lazyImage.dataset.src
}
if (lazyImage.dataset.srcset) {
lazyImage.srcset = lazyImage.dataset.srcset
}
lazyImage.classList.remove('__lazy')
cachedObserver.unobserve(lazyImage)
}
})
},
{ rootMargin: '200px' }
))
}
function computeSrc(src: string, host: string, unoptimized: boolean): string {
if (unoptimized) {
return src
......@@ -106,6 +143,8 @@ export default function Image({
sizes,
unoptimized = false,
priority = false,
lazy,
className,
...rest
}: ImageProps) {
// Sanity Checks:
......@@ -122,6 +161,15 @@ export default function Image({
}
host = 'default'
}
// If priority and lazy are present, log an error and use priority only.
if (priority && lazy) {
if (process.env.NODE_ENV !== 'production') {
console.error(
`Image with src ${src} has both priority and lazy tags. Only one should be used.`
)
}
lazy = false
}
host = host || 'default'
......@@ -130,20 +178,80 @@ export default function Image({
src = src.slice(1)
}
let thisEl: any
useEffect(() => {
if (lazy) {
const observer = getObserver()
if (observer) {
observer.observe(thisEl)
return () => {
observer.unobserve(thisEl)
}
}
}
}, [thisEl, lazy])
// Generate attribute values
const imgSrc = computeSrc(src, host, unoptimized)
const imgAttributes: { src: string; srcSet?: string } = { src: imgSrc }
let imgSrcset = null
if (!unoptimized) {
imgAttributes.srcSet = generateSrcSet({
imgSrcset = generateSrcSet({
src,
host: host,
widths: breakpoints,
})
}
const imgAttributes: {
src?: string
srcSet?: string
'data-src'?: string
'data-srcset'?: string
} = {}
if (!lazy) {
imgAttributes.src = imgSrc
if (imgSrcset) {
imgAttributes.srcSet = imgSrcset
}
} else {
imgAttributes['data-src'] = imgSrc
if (imgSrcset) {
imgAttributes['data-srcset'] = imgSrcset
}
className = className ? className + ' __lazy' : '__lazy'
}
// No need to add preloads on the client side--by the time the application is hydrated,
// it's too late for preloads
const shouldPreload = priority && typeof window === 'undefined'
let imgElement
if (className) {
imgElement = (
<img
{...rest}
ref={(el) => {
thisEl = el
}}
{...imgAttributes}
className={className}
sizes={sizes}
/>
)
} else {
imgElement = (
<img
ref={(el) => {
thisEl = el
}}
{...rest}
{...imgAttributes}
sizes={sizes}
/>
)
}
return (
<div>
{shouldPreload
......@@ -155,7 +263,7 @@ export default function Image({
sizes,
})
: ''}
<img {...rest} {...imgAttributes} sizes={sizes} />
{imgElement}
</div>
)
}
......
......@@ -36,6 +36,9 @@ const Page = () => {
<Link href="/client-side">
<a id="clientlink">Client Side</a>
</Link>
<Link href="/lazy">
<a id="lazylink">lazy</a>
</Link>
<p id="stubtext">This is the index page</p>
</div>
)
......
import React from 'react'
import Image from 'next/image'
const Lazy = () => {
return (
<div>
<p id="stubtext">This is a page with lazy-loaded images</p>
<Image
id="lazy-top"
src="foo1.jpg"
height="400px"
width="300px"
lazy
></Image>
<div style={{ height: '2000px' }}></div>
<Image
id="lazy-mid"
src="foo2.jpg"
lazy
height="400px"
width="300px"
className="exampleclass"
></Image>
<div style={{ height: '2000px' }}></div>
<Image
id="lazy-bottom"
src="https://www.otherhost.com/foo3.jpg"
height="400px"
width="300px"
unoptimized
lazy
></Image>
</div>
)
}
export default Lazy
......@@ -71,6 +71,70 @@ function runTests() {
})
}
function lazyLoadingTests() {
it('should have loaded the first image immediately', async () => {
expect(await browser.elementById('lazy-top').getAttribute('src')).toBe(
'https://example.com/myaccount/foo1.jpg'
)
expect(await browser.elementById('lazy-top').getAttribute('srcset')).toBe(
'https://example.com/myaccount/foo1.jpg?w=480 480w, https://example.com/myaccount/foo1.jpg?w=1024 1024w, https://example.com/myaccount/foo1.jpg?w=1600 1600w'
)
})
it('should not have loaded the second image immediately', async () => {
expect(
await browser.elementById('lazy-mid').getAttribute('src')
).toBeFalsy()
expect(
await browser.elementById('lazy-mid').getAttribute('srcset')
).toBeFalsy()
})
it('should pass through classes on a lazy loaded image', async () => {
expect(await browser.elementById('lazy-mid').getAttribute('class')).toBe(
'exampleclass __lazy'
)
})
it('should load the second image after scrolling down', async () => {
let viewportHeight = await browser.eval(`window.innerHeight`)
let topOfMidImage = await browser.eval(
`document.getElementById('lazy-mid').offsetTop`
)
let buffer = 150
await browser.eval(
`window.scrollTo(0, ${topOfMidImage - (viewportHeight + buffer)})`
)
expect(await browser.elementById('lazy-mid').getAttribute('src')).toBe(
'https://example.com/myaccount/foo2.jpg'
)
expect(await browser.elementById('lazy-mid').getAttribute('srcset')).toBe(
'https://example.com/myaccount/foo2.jpg?w=480 480w, https://example.com/myaccount/foo2.jpg?w=1024 1024w, https://example.com/myaccount/foo2.jpg?w=1600 1600w'
)
})
it('should not have loaded the third image after scrolling down', async () => {
expect(
await browser.elementById('lazy-bottom').getAttribute('src')
).toBeFalsy()
expect(
await browser.elementById('lazy-bottom').getAttribute('srcset')
).toBeFalsy()
})
it('should load the third image, which is unoptimized, after scrolling further down', async () => {
let viewportHeight = await browser.eval(`window.innerHeight`)
let topOfBottomImage = await browser.eval(
`document.getElementById('lazy-bottom').offsetTop`
)
let buffer = 150
await browser.eval(
`window.scrollTo(0, ${topOfBottomImage - (viewportHeight + buffer)})`
)
expect(await browser.elementById('lazy-bottom').getAttribute('src')).toBe(
'https://www.otherhost.com/foo3.jpg'
)
expect(
await browser.elementById('lazy-bottom').getAttribute('srcset')
).toBeFalsy()
})
}
async function hasPreloadLinkMatchingUrl(url) {
const links = await browser.elementsByCss('link')
let foundMatch = false
......@@ -165,4 +229,24 @@ describe('Image Component Tests', () => {
})
})
})
describe('SSR Lazy Loading Tests', () => {
beforeAll(async () => {
browser = await webdriver(appPort, '/lazy')
})
afterAll(async () => {
browser = null
})
lazyLoadingTests()
})
describe('Client-side Lazy Loading Tests', () => {
beforeAll(async () => {
browser = await webdriver(appPort, '/')
await browser.waitForElementByCss('#lazylink').click()
await waitFor(500)
})
afterAll(async () => {
browser = null
})
lazyLoadingTests()
})
})
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册