Skip to content

Commit

Permalink
Add errors for invalid placeholder=blur usage (#25953)
Browse files Browse the repository at this point in the history
There are strict conditions for using `placeholder=blur` documented in #25949 but this will give the user a better understanding during `next dev` and links to the error.

- Error when `placeholder=blur` and no `blurDataURL`
- The Error for small images with `placeholder=blur` has been changed to a warning
- Added support for blurring a webp image
- Added error page linking to relevant docs
  • Loading branch information
styfle committed Jun 10, 2021
1 parent 2cff809 commit d8b59f3
Show file tree
Hide file tree
Showing 11 changed files with 146 additions and 45 deletions.
15 changes: 15 additions & 0 deletions errors/placeholder-blur-data-url.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# `placeholder=blur` without `blurDataURL`

#### Why This Error Occurred

You are attempting use the `next/image` component with `placeholder=blur` property but no `blurDataURL` property.

The `blurDataURL` might be missing because your using a string for `src` instead of a static import.

Or `blurDataURL` might be missing because the static import is an unsupported image format. Only jpg, png, and webp are supported at this time.

#### Possible Ways to Fix It

- Add a [`blurDataURL`](https://nextjs.org/docs/api-reference/next/image#blurdataurl) property, the contents should be a small Data URL to represent the image
- Change the [`src`](https://nextjs.org/docs/api-reference/next/image#src) property to a static import with one of the supported file types: jpg, png, or webp
- Remove the [`placeholder`](https://nextjs.org/docs/api-reference/next/image#placeholder) property, effectively no blur effect
20 changes: 11 additions & 9 deletions packages/next/build/webpack/loaders/next-image-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import loaderUtils from 'next/dist/compiled/loader-utils'
import sizeOf from 'image-size'
import { processBuffer } from '../../../next-server/server/lib/squoosh/main'

const PLACEHOLDER_SIZE = 8
const BLUR_IMG_SIZE = 8
const BLUR_QUALITY = 70
const VALID_BLUR_EXT = ['jpeg', 'png', 'webp']

async function nextImageLoader(content) {
const context = this.rootContext
Expand All @@ -19,20 +21,20 @@ async function nextImageLoader(content) {
}

const imageSize = sizeOf(content)
let placeholder
if (extension === 'jpeg' || extension === 'png') {
// Shrink the image's largest dimension to 6 pixels
let blurDataURL
if (VALID_BLUR_EXT.includes(extension)) {
// Shrink the image's largest dimension
const resizeOperationOpts =
imageSize.width >= imageSize.height
? { type: 'resize', width: PLACEHOLDER_SIZE }
: { type: 'resize', height: PLACEHOLDER_SIZE }
? { type: 'resize', width: BLUR_IMG_SIZE }
: { type: 'resize', height: BLUR_IMG_SIZE }
const resizedImage = await processBuffer(
content,
[resizeOperationOpts],
extension,
70
BLUR_QUALITY
)
placeholder = `data:image/${extension};base64,${resizedImage.toString(
blurDataURL = `data:image/${extension};base64,${resizedImage.toString(
'base64'
)}`
}
Expand All @@ -41,7 +43,7 @@ async function nextImageLoader(content) {
src: '/_next' + interpolatedName,
height: imageSize.height,
width: imageSize.width,
placeholder,
blurDataURL,
})

this.emitFile(interpolatedName, content, null)
Expand Down
41 changes: 28 additions & 13 deletions packages/next/client/image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ interface StaticImageData {
src: string
height: number
width: number
placeholder?: string
blurDataURL?: string
}

interface StaticRequire {
Expand Down Expand Up @@ -329,9 +329,7 @@ export default function Image({
)}`
)
}
if (staticImageData.placeholder) {
blurDataURL = staticImageData.placeholder
}
blurDataURL = blurDataURL || staticImageData.blurDataURL
staticSrc = staticImageData.src
if (!layout || layout !== 'fill') {
height = height || staticImageData.height
Expand All @@ -347,6 +345,10 @@ export default function Image({
}
src = typeof src === 'string' ? src : staticSrc

const widthInt = getInt(width)
const heightInt = getInt(height)
const qualityInt = getInt(quality)

if (process.env.NODE_ENV !== 'production') {
if (!src) {
throw new Error(
Expand Down Expand Up @@ -374,6 +376,27 @@ export default function Image({
`Image with src "${src}" has both "priority" and "loading='lazy'" properties. Only one should be used.`
)
}
if (placeholder === 'blur') {
if ((widthInt || 0) * (heightInt || 0) < 1600) {
console.warn(
`Image with src "${src}" is smaller than 40x40. Consider removing the "placeholder='blur'" property to improve performance.`
)
}
if (!blurDataURL) {
const VALID_BLUR_EXT = ['jpeg', 'png', 'webp'] // should match next-image-loader

throw new Error(
`Image with src "${src}" has "placeholder='blur'" property but is missing the "blurDataURL" property.
Possible solutions:
- Add a "blurDataURL" property, the contents should be a small Data URL to represent the image
- Change the "src" property to a static import with one of the supported file types: ${VALID_BLUR_EXT.join(
','
)}
- Remove the "placeholder" property, effectively no blur effect
Read more: https://nextjs.org/docs/messages/placeholder-blur-data-url`
)
}
}
}
let isLazy =
!priority && (loading === 'lazy' || typeof loading === 'undefined')
Expand All @@ -389,14 +412,6 @@ export default function Image({
})
const isVisible = !isLazy || isIntersected

const widthInt = getInt(width)
const heightInt = getInt(height)
const qualityInt = getInt(quality)

// Show blur if larger than 5000px such as 100 x 50
const showBlurPlaceholder =
placeholder === 'blur' && (widthInt || 0) * (heightInt || 0) > 5000

let wrapperStyle: JSX.IntrinsicElements['div']['style'] | undefined
let sizerStyle: JSX.IntrinsicElements['div']['style'] | undefined
let sizerSvg: string | undefined
Expand All @@ -423,7 +438,7 @@ export default function Image({
objectFit,
objectPosition,

...(showBlurPlaceholder
...(placeholder === 'blur'
? {
filter: 'blur(20px)',
backgroundSize: 'cover',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export default function Page() {
<Image
priority
id="blurry-placeholder"
src="/test.jpg"
src="/test.ico"
width="400"
height="400"
placeholder="blur"
Expand All @@ -19,7 +19,7 @@ export default function Page() {

<Image
id="blurry-placeholder-with-lazy"
src="/test.jpg"
src="/test.bmp"
width="400"
height="400"
placeholder="blur"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React from 'react'
import Image from 'next/image'
import testBMP from '../public/test.bmp'

const Page = () => {
return (
<div>
<Image
id="invalid-placeholder-blur-static"
src={testBMP}
placeholder="blur"
/>
</div>
)
}

export default Page
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import React from 'react'
import Image from 'next/image'

const Page = () => {
return (
<div>
<Image id="invalid-placeholder-blur" src="/test.png" placeholder="blur" />
</div>
)
}

export default Page
13 changes: 13 additions & 0 deletions test/integration/image-component/default/pages/small-img-import.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React from 'react'
import Image from 'next/image'
import Small from '../public/small.jpg'

const Page = () => {
return (
<div>
<Image id="small-img-import" src={Small} placeholder="blur" />
</div>
)
}

export default Page
23 changes: 9 additions & 14 deletions test/integration/image-component/default/pages/static.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,13 @@ import Image from 'next/image'

import testJPG from '../public/test.jpg'
import testPNG from '../public/test.png'
import testWEBP from '../public/test.webp'
import testSVG from '../public/test.svg'
import testGIF from '../public/test.gif'
import testBMP from '../public/test.bmp'
import testICO from '../public/test.ico'
import testWEBP from '../public/test.webp'

import TallImage from '../components/TallImage'
const testFiles = [
testJPG,
testPNG,
testSVG,
testGIF,
testBMP,
testICO,
testWEBP,
]

const Page = () => {
return (
<div>
Expand All @@ -46,9 +36,14 @@ const Page = () => {
width="400"
height="300"
/>
{testFiles.map((f, i) => (
<Image id={`format-test-${i}`} key={i} src={f} placeholder="blur" />
))}
<br />
<Image id="blur-png" src={testPNG} placeholder="blur" />
<Image id="blur-jpg" src={testJPG} placeholder="blur" />
<Image id="blur-webp" src={testWEBP} placeholder="blur" />
<Image id="static-svg" src={testSVG} />
<Image id="static-gif" src={testGIF} />
<Image id="static-bmp" src={testBMP} />
<Image id="static-ico" src={testICO} />
</div>
)
}
Expand Down
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
33 changes: 33 additions & 0 deletions test/integration/image-component/default/test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,39 @@ function runTests(mode) {
'Failed to parse src "//assets.example.com/img.jpg" on `next/image`, protocol-relative URL (//) must be changed to an absolute URL (http:// or https://)'
)
})

it('should show error when string src and placeholder=blur and blurDataURL is missing', async () => {
const browser = await webdriver(appPort, '/invalid-placeholder-blur')

expect(await hasRedbox(browser)).toBe(true)
expect(await getRedboxHeader(browser)).toContain(
`Image with src "/test.png" has "placeholder='blur'" property but is missing the "blurDataURL" property.`
)
})

it('should show error when static import and placeholder=blur and blurDataUrl is missing', async () => {
const browser = await webdriver(
appPort,
'/invalid-placeholder-blur-static'
)

expect(await hasRedbox(browser)).toBe(true)
expect(await getRedboxHeader(browser)).toMatch(
/Image with src "(.*)bmp" has "placeholder='blur'" property but is missing the "blurDataURL" property/
)
})

it('should warn when using a very small image with placeholder=blur', async () => {
const browser = await webdriver(appPort, '/small-img-import')

const warnings = (await browser.log('browser'))
.map((log) => log.message)
.join('\n')
expect(await hasRedbox(browser)).toBe(false)
expect(warnings).toMatch(
/Image with src (.*)jpg(.*) is smaller than 40x40. Consider removing(.*)/gm
)
})
}

it('should correctly ignore prose styles', async () => {
Expand Down
13 changes: 6 additions & 7 deletions test/integration/image-component/default/test/static.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,12 @@ const indexPage = new File(join(appDir, 'pages/static.js'))
const runTests = () => {
it('Should allow an image with a static src to omit height and width', async () => {
expect(await browser.elementById('basic-static')).toBeTruthy()
expect(await browser.elementById('format-test-0')).toBeTruthy()
expect(await browser.elementById('format-test-1')).toBeTruthy()
expect(await browser.elementById('format-test-2')).toBeTruthy()
expect(await browser.elementById('format-test-3')).toBeTruthy()
expect(await browser.elementById('format-test-4')).toBeTruthy()
expect(await browser.elementById('format-test-5')).toBeTruthy()
expect(await browser.elementById('format-test-6')).toBeTruthy()
expect(await browser.elementById('blur-png')).toBeTruthy()
expect(await browser.elementById('blur-jpg')).toBeTruthy()
expect(await browser.elementById('static-svg')).toBeTruthy()
expect(await browser.elementById('static-gif')).toBeTruthy()
expect(await browser.elementById('static-bmp')).toBeTruthy()
expect(await browser.elementById('static-ico')).toBeTruthy()
})
it('Should automatically provide an image height and width', async () => {
expect(html).toContain('width:400px;height:300px')
Expand Down

0 comments on commit d8b59f3

Please sign in to comment.