Skip to content

Commit

Permalink
Remove useState from next/image (#43587)
Browse files Browse the repository at this point in the history
This PR remove `React.useState()` from the `next/image` component. It
was only used in the `onError` case and it was causing Safari to become
very slow when there were many images on the same page. We were seeing
1s delay blocking the main thread when there were about 350 images on
the same page. Chrome and Firefox were not slow.
  • Loading branch information
styfle committed Dec 1, 2022
1 parent b0aa73b commit 7a7e7d4
Show file tree
Hide file tree
Showing 4 changed files with 43 additions and 141 deletions.
47 changes: 19 additions & 28 deletions packages/next/client/image.tsx
Expand Up @@ -6,7 +6,6 @@ import React, {
useCallback,
useContext,
useMemo,
useState,
forwardRef,
} from 'react'
import Head from '../shared/lib/head'
Expand Down Expand Up @@ -149,8 +148,6 @@ type ImageElementProps = Omit<ImageProps, 'src' | 'alt' | 'loader'> & {
placeholder: PlaceholderValue
onLoadRef: React.MutableRefObject<OnLoad | undefined>
onLoadingCompleteRef: React.MutableRefObject<OnLoadingComplete | undefined>
setBlurComplete: (b: boolean) => void
setShowAltText: (b: boolean) => void
}

function getWidths(
Expand Down Expand Up @@ -264,10 +261,8 @@ function getInt(x: unknown): number | undefined {
function handleLoading(
img: ImgElementWithDataProp,
src: string,
placeholder: PlaceholderValue,
onLoadRef: React.MutableRefObject<OnLoad | undefined>,
onLoadingCompleteRef: React.MutableRefObject<OnLoadingComplete | undefined>,
setBlurComplete: (b: boolean) => void,
unoptimized: boolean
) {
if (!img || img['data-loaded-src'] === src) {
Expand All @@ -284,9 +279,6 @@ function handleLoading(
// - decode() completes
return
}
if (placeholder === 'blur') {
setBlurComplete(true)
}
if (onLoadRef?.current) {
// Since we don't have the SyntheticEvent here,
// we must create one with the same shape.
Expand Down Expand Up @@ -383,8 +375,6 @@ const ImageElement = forwardRef<HTMLImageElement | null, ImageElementProps>(
loader,
onLoadRef,
onLoadingCompleteRef,
setBlurComplete,
setShowAltText,
onLoad,
onError,
...rest
Expand Down Expand Up @@ -441,20 +431,16 @@ const ImageElement = forwardRef<HTMLImageElement | null, ImageElementProps>(
handleLoading(
img,
srcString,
placeholder,
onLoadRef,
onLoadingCompleteRef,
setBlurComplete,
unoptimized
)
}
},
[
srcString,
placeholder,
onLoadRef,
onLoadingCompleteRef,
setBlurComplete,
onError,
unoptimized,
forwardedRef,
Expand All @@ -465,20 +451,30 @@ const ImageElement = forwardRef<HTMLImageElement | null, ImageElementProps>(
handleLoading(
img,
srcString,
placeholder,
onLoadRef,
onLoadingCompleteRef,
setBlurComplete,
unoptimized
)
}}
onError={(event) => {
// if the real image fails to load, this will ensure "alt" is visible
setShowAltText(true)
if (placeholder === 'blur') {
// If the real image fails to load, this will still remove the placeholder.
setBlurComplete(true)
// Note: We removed React.useState() in the error case here
// because it was causing Safari to become very slow when
// there were many images on the same page.
const { style } = event.currentTarget

if (style.color === 'transparent') {
// If src image fails to load, this will ensure "alt" is visible
style.color = ''
}

if (placeholder === 'blur' && style.backgroundImage) {
// If src image fails to load, this will ensure the placeholder is removed
style.backgroundSize = ''
style.backgroundPosition = ''
style.backgroundRepeat = ''
style.backgroundImage = ''
}

if (onError) {
onError(event)
}
Expand Down Expand Up @@ -626,9 +622,6 @@ const Image = forwardRef<HTMLImageElement | null, ImageProps>(
unoptimized = true
}

const [blurComplete, setBlurComplete] = useState(false)
const [showAltText, setShowAltText] = useState(false)

const qualityInt = getInt(quality)

if (process.env.NODE_ENV !== 'production') {
Expand Down Expand Up @@ -810,12 +803,12 @@ const Image = forwardRef<HTMLImageElement | null, ImageProps>(
objectPosition,
}
: {},
showAltText ? {} : { color: 'transparent' },
{ color: 'transparent' },
style
)

const blurStyle =
placeholder === 'blur' && blurDataURL && !blurComplete
placeholder === 'blur' && blurDataURL
? {
backgroundSize: imgStyle.objectFit || 'cover',
backgroundPosition: imgStyle.objectPosition || '50% 50%',
Expand Down Expand Up @@ -905,8 +898,6 @@ const Image = forwardRef<HTMLImageElement | null, ImageProps>(
srcString,
onLoadRef,
onLoadingCompleteRef,
setBlurComplete,
setShowAltText,
...rest,
}
return (
Expand Down

This file was deleted.

66 changes: 4 additions & 62 deletions test/integration/next-image-new/default/test/index.test.ts
Expand Up @@ -599,8 +599,8 @@ function runTests(mode) {
'lazy'
)
expect(await browser.elementById('blur1').getAttribute('sizes')).toBeNull()
expect(await browser.elementById('blur1').getAttribute('style')).toBe(
'color: transparent;'
expect(await browser.elementById('blur1').getAttribute('style')).toMatch(
'color:transparent;background-size:cover;background-position:50% 50%;background-repeat:no-repeat;'
)
expect(await browser.elementById('blur1').getAttribute('height')).toBe(
'400'
Expand Down Expand Up @@ -646,8 +646,8 @@ function runTests(mode) {
expect(await browser.elementById('blur2').getAttribute('loading')).toBe(
'lazy'
)
expect(await browser.elementById('blur2').getAttribute('style')).toBe(
'color: transparent;'
expect(await browser.elementById('blur2').getAttribute('style')).toMatch(
'color:transparent;background-size:cover;background-position:50% 50%;background-repeat:no-repeat'
)
expect(await browser.elementById('blur2').getAttribute('height')).toBe(
'400'
Expand Down Expand Up @@ -1084,15 +1084,6 @@ function runTests(mode) {
expect(await getComputedStyle(browser, 'img-blur', 'filter')).toBe(
'opacity(0.5)'
)
expect(await getComputedStyle(browser, 'img-blur', 'background-size')).toBe(
'30%'
)
expect(
await getComputedStyle(browser, 'img-blur', 'background-image')
).toMatch('iVBORw0KGgo=')
expect(
await getComputedStyle(browser, 'img-blur', 'background-position')
).toBe('1px 2px')
})
describe('Fill-mode tests', () => {
let browser
Expand Down Expand Up @@ -1187,55 +1178,6 @@ function runTests(mode) {
})
}

it('should have blurry placeholder when enabled', async () => {
const html = await renderViaHTTP(appPort, '/blurry-placeholder')
const $html = cheerio.load(html)

$html('noscript > img').attr('id', 'unused')

expect($html('#blurry-placeholder-raw')[0].attribs.style).toContain(
`color:transparent;background-size:cover;background-position:50% 50%;background-repeat:no-repeat;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http%3A//www.w3.org/2000/svg' viewBox='0 0 400 400'%3E%3Cfilter id='b' color-interpolation-filters='sRGB'%3E%3CfeGaussianBlur stdDeviation='20'/%3E%3C/filter%3E%3Cimage preserveAspectRatio='none' filter='url(%23b)' x='0' y='0' height='100%25' width='100%25' href='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8P4nhDwAGuAKPn6cicwAAAABJRU5ErkJggg=='/%3E%3C/svg%3E")`
)

expect($html('#blurry-placeholder-with-lazy')[0].attribs.style).toContain(
`color:transparent;background-size:cover;background-position:50% 50%;background-repeat:no-repeat;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http%3A//www.w3.org/2000/svg' viewBox='0 0 400 400'%3E%3Cfilter id='b' color-interpolation-filters='sRGB'%3E%3CfeGaussianBlur stdDeviation='20'/%3E%3C/filter%3E%3Cimage preserveAspectRatio='none' filter='url(%23b)' x='0' y='0' height='100%25' width='100%25' href='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mO0/8/wBwAE/wI85bEJ6gAAAABJRU5ErkJggg=='/%3E%3C/svg%3E")`
)
})

it('should remove blurry placeholder after image loads', async () => {
const browser = await webdriver(appPort, '/blurry-placeholder')
await check(
async () =>
await getComputedStyle(
browser,
'blurry-placeholder-raw',
'background-image'
),
'none'
)
expect(
await getComputedStyle(
browser,
'blurry-placeholder-with-lazy',
'background-image'
)
).toBe(
`url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http%3A//www.w3.org/2000/svg' viewBox='0 0 400 400'%3E%3Cfilter id='b' color-interpolation-filters='sRGB'%3E%3CfeGaussianBlur stdDeviation='20'/%3E%3C/filter%3E%3Cimage preserveAspectRatio='none' filter='url(%23b)' x='0' y='0' height='100%25' width='100%25' href='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mO0/8/wBwAE/wI85bEJ6gAAAABJRU5ErkJggg=='/%3E%3C/svg%3E")`
)

await browser.eval('document.getElementById("spacer").remove()')

await check(
async () =>
await getComputedStyle(
browser,
'blurry-placeholder-with-lazy',
'background-image'
),
'none'
)
})

it('should be valid HTML', async () => {
let browser
try {
Expand Down
40 changes: 20 additions & 20 deletions test/integration/production/test/index.test.js
Expand Up @@ -1301,7 +1301,7 @@ describe('Production Usage', () => {
})
}

it('should remove placeholder for next/image correctly', async () => {
it('should loading next/image correctly', async () => {
const browser = await webdriver(context.appPort, '/')

await browser.eval(`(function() {
Expand All @@ -1312,10 +1312,16 @@ describe('Production Usage', () => {

expect(await browser.eval('window.beforeNav')).toBe(1)

await check(
() => browser.elementByCss('img').getComputedCss('background-image'),
'none'
)
await check(async () => {
const result = await browser.eval(
`document.getElementById('static-image').width`
)
if (result === 0) {
throw new Error('Incorrectly loaded image')
}

return 'result-correct'
}, /result-correct/)

await browser.eval(`(function() {
window.beforeNav = 1
Expand All @@ -1332,22 +1338,16 @@ describe('Production Usage', () => {

expect(await browser.eval('window.beforeNav')).toBe(1)

await check(
() =>
browser
.elementByCss('#static-image')
.getComputedCss('background-image'),
'none'
)
await check(async () => {
const result = await browser.eval(
`document.getElementById('static-image').width`
)
if (result === 0) {
throw new Error('Incorrectly loaded image')
}

for (let i = 0; i < 5; i++) {
expect(
await browser
.elementByCss('#static-image')
.getComputedCss('background-image')
).toBe('none')
await waitFor(500)
}
return 'result-correct'
}, /result-correct/)
})

dynamicImportTests(context, (p, q) => renderViaHTTP(context.appPort, p, q))
Expand Down

0 comments on commit 7a7e7d4

Please sign in to comment.