Skip to content

Commit

Permalink
Revert "Remove useState from next/image (#43587)" (#44094)
Browse files Browse the repository at this point in the history
# Reverts #43587

PR #43587 breaks the `placeholder="blur"` property on the `<Image />`
component by keeping the `blurStyles`, e.g. the blurred image, after the
image is loaded.

**This regression does _not_ introduce any breaking changes or bugs.**

---

The reason for the original PR was:
> 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.

The original PR is a performance improvement for Safari on a corner
case.

Additionally, when tackling this performance improvement again, the
`blurStyle` needs to know when the the image is done loading so it can
get rid of the blur. The state is updated in `handeLoading()` and isn't
just used `onError`.

## Fixes issues

- Fixes #43829
- Fixes #43689

## To reproduce

For reference this when #43587 was pulled into Next.js
[v13.0.6-canary.3](https://github.com/vercel/next.js/blob/v13.0.6-canary.3/packages/next/client/image.tsx)

- Regress the `image.tsx` to
[v13.0.6-canary.2](https://github.com/vercel/next.js/blob/v13.0.6-canary.2/packages/next/client/image.tsx)
- Do a local build with the regressed `image.tsx` on (current canary
build)
[v13.0.8-canary.0](https://github.com/vercel/next.js/releases/tag/v13.0.8-canary.0)
- Example code, (import any image you like) make sure to use
`placeholder="blur"`
```typescript
import Image from 'next/image'
import CatImage from '../public/cat.png'

<Image
    src={CatImage}
    width={500}
    height={500}
    alt="Cat"
    priority
    placeholder="blur"
/>
```
- Image will still have the blur after the image is loaded
- Before and after screenshot


![before](https://user-images.githubusercontent.com/1037693/208206084-bd6fa143-ca19-4fda-9f4e-8fcec9836848.png)


![after](https://user-images.githubusercontent.com/229881/208470446-3a00eac6-f82e-4017-bd9f-7c6145456959.png)

Co-authored-by: Steven <steven@ceriously.com>
  • Loading branch information
l1qu1d and styfle committed Dec 19, 2022
1 parent fa880d0 commit 723311b
Show file tree
Hide file tree
Showing 4 changed files with 141 additions and 43 deletions.
47 changes: 28 additions & 19 deletions packages/next/client/image.tsx
Expand Up @@ -6,6 +6,7 @@ import React, {
useCallback,
useContext,
useMemo,
useState,
forwardRef,
} from 'react'
import Head from '../shared/lib/head'
Expand Down Expand Up @@ -148,6 +149,8 @@ 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 @@ -261,8 +264,10 @@ 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 @@ -279,6 +284,9 @@ 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 @@ -375,6 +383,8 @@ const ImageElement = forwardRef<HTMLImageElement | null, ImageElementProps>(
loader,
onLoadRef,
onLoadingCompleteRef,
setBlurComplete,
setShowAltText,
onLoad,
onError,
...rest
Expand Down Expand Up @@ -431,16 +441,20 @@ const ImageElement = forwardRef<HTMLImageElement | null, ImageElementProps>(
handleLoading(
img,
srcString,
placeholder,
onLoadRef,
onLoadingCompleteRef,
setBlurComplete,
unoptimized
)
}
},
[
srcString,
placeholder,
onLoadRef,
onLoadingCompleteRef,
setBlurComplete,
onError,
unoptimized,
forwardedRef,
Expand All @@ -451,30 +465,20 @@ const ImageElement = forwardRef<HTMLImageElement | null, ImageElementProps>(
handleLoading(
img,
srcString,
placeholder,
onLoadRef,
onLoadingCompleteRef,
setBlurComplete,
unoptimized
)
}}
onError={(event) => {
// 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 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)
}

if (onError) {
onError(event)
}
Expand Down Expand Up @@ -622,6 +626,9 @@ 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 @@ -803,12 +810,12 @@ const Image = forwardRef<HTMLImageElement | null, ImageProps>(
objectPosition,
}
: {},
{ color: 'transparent' },
showAltText ? {} : { color: 'transparent' },
style
)

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

export default function Page() {
return (
<div>
<p>Blurry Placeholder</p>

<Image
priority
id="blurry-placeholder-raw"
src="/test.ico"
width="400"
height="400"
placeholder="blur"
blurDataURL="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8P4nhDwAGuAKPn6cicwAAAABJRU5ErkJggg=="
/>

<div id="spacer" style={{ height: '1000vh' }} />

<Image
id="blurry-placeholder-with-lazy"
src="/test.bmp"
width="400"
height="400"
placeholder="blur"
blurDataURL="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mO0/8/wBwAE/wI85bEJ6gAAAABJRU5ErkJggg=="
/>
</div>
)
}
66 changes: 62 additions & 4 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')).toMatch(
'color:transparent;background-size:cover;background-position:50% 50%;background-repeat:no-repeat;'
expect(await browser.elementById('blur1').getAttribute('style')).toBe(
'color: transparent;'
)
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')).toMatch(
'color:transparent;background-size:cover;background-position:50% 50%;background-repeat:no-repeat'
expect(await browser.elementById('blur2').getAttribute('style')).toBe(
'color: transparent;'
)
expect(await browser.elementById('blur2').getAttribute('height')).toBe(
'400'
Expand Down Expand Up @@ -1084,6 +1084,15 @@ 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 @@ -1178,6 +1187,55 @@ 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 loading next/image correctly', async () => {
it('should remove placeholder for next/image correctly', async () => {
const browser = await webdriver(context.appPort, '/')

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

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

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 check(
() => browser.elementByCss('img').getComputedCss('background-image'),
'none'
)

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

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

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

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

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

0 comments on commit 723311b

Please sign in to comment.