From 66c2e773c9bfeaa103450ab1cc9d4063357bd67c Mon Sep 17 00:00:00 2001 From: l1qu1d <1037693+l1qu1d@users.noreply.github.com> Date: Mon, 19 Dec 2022 11:10:23 -0600 Subject: [PATCH] Revert "Remove `useState` from `next/image` (#43587)" (#44094) # Reverts vercel/next.js#43587 PR #43587 breaks the `placeholder="blur"` property on the `` 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' Cat ``` - 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 --- packages/next/client/image.tsx | 47 +++++++------ .../default/pages/blurry-placeholder.js | 31 +++++++++ .../next-image-new/default/test/index.test.ts | 66 +++++++++++++++++-- .../integration/production/test/index.test.js | 40 +++++------ 4 files changed, 141 insertions(+), 43 deletions(-) create mode 100644 test/integration/next-image-new/default/pages/blurry-placeholder.js diff --git a/packages/next/client/image.tsx b/packages/next/client/image.tsx index 6a231114fc8312f..fcde56b2d4b3c3c 100644 --- a/packages/next/client/image.tsx +++ b/packages/next/client/image.tsx @@ -6,6 +6,7 @@ import React, { useCallback, useContext, useMemo, + useState, forwardRef, } from 'react' import Head from '../shared/lib/head' @@ -148,6 +149,8 @@ type ImageElementProps = Omit & { placeholder: PlaceholderValue onLoadRef: React.MutableRefObject onLoadingCompleteRef: React.MutableRefObject + setBlurComplete: (b: boolean) => void + setShowAltText: (b: boolean) => void } function getWidths( @@ -261,8 +264,10 @@ function getInt(x: unknown): number | undefined { function handleLoading( img: ImgElementWithDataProp, src: string, + placeholder: PlaceholderValue, onLoadRef: React.MutableRefObject, onLoadingCompleteRef: React.MutableRefObject, + setBlurComplete: (b: boolean) => void, unoptimized: boolean ) { if (!img || img['data-loaded-src'] === src) { @@ -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. @@ -375,6 +383,8 @@ const ImageElement = forwardRef( loader, onLoadRef, onLoadingCompleteRef, + setBlurComplete, + setShowAltText, onLoad, onError, ...rest @@ -431,16 +441,20 @@ const ImageElement = forwardRef( handleLoading( img, srcString, + placeholder, onLoadRef, onLoadingCompleteRef, + setBlurComplete, unoptimized ) } }, [ srcString, + placeholder, onLoadRef, onLoadingCompleteRef, + setBlurComplete, onError, unoptimized, forwardedRef, @@ -451,30 +465,20 @@ const ImageElement = forwardRef( 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) } @@ -622,6 +626,9 @@ const Image = forwardRef( unoptimized = true } + const [blurComplete, setBlurComplete] = useState(false) + const [showAltText, setShowAltText] = useState(false) + const qualityInt = getInt(quality) if (process.env.NODE_ENV !== 'production') { @@ -803,12 +810,12 @@ const Image = forwardRef( 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%', @@ -898,6 +905,8 @@ const Image = forwardRef( srcString, onLoadRef, onLoadingCompleteRef, + setBlurComplete, + setShowAltText, ...rest, } return ( diff --git a/test/integration/next-image-new/default/pages/blurry-placeholder.js b/test/integration/next-image-new/default/pages/blurry-placeholder.js new file mode 100644 index 000000000000000..30583ad0a20a2ce --- /dev/null +++ b/test/integration/next-image-new/default/pages/blurry-placeholder.js @@ -0,0 +1,31 @@ +import React from 'react' +import Image from 'next/image' + +export default function Page() { + return ( +
+

Blurry Placeholder

+ + + +
+ + +
+ ) +} diff --git a/test/integration/next-image-new/default/test/index.test.ts b/test/integration/next-image-new/default/test/index.test.ts index 9e4ff54778a7cc2..65892992a95c015 100644 --- a/test/integration/next-image-new/default/test/index.test.ts +++ b/test/integration/next-image-new/default/test/index.test.ts @@ -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' @@ -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' @@ -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 @@ -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 { diff --git a/test/integration/production/test/index.test.js b/test/integration/production/test/index.test.js index c47d51ea0614bc2..2650bd523cabb2b 100644 --- a/test/integration/production/test/index.test.js +++ b/test/integration/production/test/index.test.js @@ -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() { @@ -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 @@ -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))