Skip to content

Commit d8b59f3

Browse files
authored
Add errors for invalid placeholder=blur usage (#25953)
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
1 parent 2cff809 commit d8b59f3

File tree

11 files changed

+146
-45
lines changed

11 files changed

+146
-45
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# `placeholder=blur` without `blurDataURL`
2+
3+
#### Why This Error Occurred
4+
5+
You are attempting use the `next/image` component with `placeholder=blur` property but no `blurDataURL` property.
6+
7+
The `blurDataURL` might be missing because your using a string for `src` instead of a static import.
8+
9+
Or `blurDataURL` might be missing because the static import is an unsupported image format. Only jpg, png, and webp are supported at this time.
10+
11+
#### Possible Ways to Fix It
12+
13+
- 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
14+
- 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
15+
- Remove the [`placeholder`](https://nextjs.org/docs/api-reference/next/image#placeholder) property, effectively no blur effect

packages/next/build/webpack/loaders/next-image-loader.js

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ import loaderUtils from 'next/dist/compiled/loader-utils'
22
import sizeOf from 'image-size'
33
import { processBuffer } from '../../../next-server/server/lib/squoosh/main'
44

5-
const PLACEHOLDER_SIZE = 8
5+
const BLUR_IMG_SIZE = 8
6+
const BLUR_QUALITY = 70
7+
const VALID_BLUR_EXT = ['jpeg', 'png', 'webp']
68

79
async function nextImageLoader(content) {
810
const context = this.rootContext
@@ -19,20 +21,20 @@ async function nextImageLoader(content) {
1921
}
2022

2123
const imageSize = sizeOf(content)
22-
let placeholder
23-
if (extension === 'jpeg' || extension === 'png') {
24-
// Shrink the image's largest dimension to 6 pixels
24+
let blurDataURL
25+
if (VALID_BLUR_EXT.includes(extension)) {
26+
// Shrink the image's largest dimension
2527
const resizeOperationOpts =
2628
imageSize.width >= imageSize.height
27-
? { type: 'resize', width: PLACEHOLDER_SIZE }
28-
: { type: 'resize', height: PLACEHOLDER_SIZE }
29+
? { type: 'resize', width: BLUR_IMG_SIZE }
30+
: { type: 'resize', height: BLUR_IMG_SIZE }
2931
const resizedImage = await processBuffer(
3032
content,
3133
[resizeOperationOpts],
3234
extension,
33-
70
35+
BLUR_QUALITY
3436
)
35-
placeholder = `data:image/${extension};base64,${resizedImage.toString(
37+
blurDataURL = `data:image/${extension};base64,${resizedImage.toString(
3638
'base64'
3739
)}`
3840
}
@@ -41,7 +43,7 @@ async function nextImageLoader(content) {
4143
src: '/_next' + interpolatedName,
4244
height: imageSize.height,
4345
width: imageSize.width,
44-
placeholder,
46+
blurDataURL,
4547
})
4648

4749
this.emitFile(interpolatedName, content, null)

packages/next/client/image.tsx

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ interface StaticImageData {
5353
src: string
5454
height: number
5555
width: number
56-
placeholder?: string
56+
blurDataURL?: string
5757
}
5858

5959
interface StaticRequire {
@@ -329,9 +329,7 @@ export default function Image({
329329
)}`
330330
)
331331
}
332-
if (staticImageData.placeholder) {
333-
blurDataURL = staticImageData.placeholder
334-
}
332+
blurDataURL = blurDataURL || staticImageData.blurDataURL
335333
staticSrc = staticImageData.src
336334
if (!layout || layout !== 'fill') {
337335
height = height || staticImageData.height
@@ -347,6 +345,10 @@ export default function Image({
347345
}
348346
src = typeof src === 'string' ? src : staticSrc
349347

348+
const widthInt = getInt(width)
349+
const heightInt = getInt(height)
350+
const qualityInt = getInt(quality)
351+
350352
if (process.env.NODE_ENV !== 'production') {
351353
if (!src) {
352354
throw new Error(
@@ -374,6 +376,27 @@ export default function Image({
374376
`Image with src "${src}" has both "priority" and "loading='lazy'" properties. Only one should be used.`
375377
)
376378
}
379+
if (placeholder === 'blur') {
380+
if ((widthInt || 0) * (heightInt || 0) < 1600) {
381+
console.warn(
382+
`Image with src "${src}" is smaller than 40x40. Consider removing the "placeholder='blur'" property to improve performance.`
383+
)
384+
}
385+
if (!blurDataURL) {
386+
const VALID_BLUR_EXT = ['jpeg', 'png', 'webp'] // should match next-image-loader
387+
388+
throw new Error(
389+
`Image with src "${src}" has "placeholder='blur'" property but is missing the "blurDataURL" property.
390+
Possible solutions:
391+
- Add a "blurDataURL" property, the contents should be a small Data URL to represent the image
392+
- Change the "src" property to a static import with one of the supported file types: ${VALID_BLUR_EXT.join(
393+
','
394+
)}
395+
- Remove the "placeholder" property, effectively no blur effect
396+
Read more: https://nextjs.org/docs/messages/placeholder-blur-data-url`
397+
)
398+
}
399+
}
377400
}
378401
let isLazy =
379402
!priority && (loading === 'lazy' || typeof loading === 'undefined')
@@ -389,14 +412,6 @@ export default function Image({
389412
})
390413
const isVisible = !isLazy || isIntersected
391414

392-
const widthInt = getInt(width)
393-
const heightInt = getInt(height)
394-
const qualityInt = getInt(quality)
395-
396-
// Show blur if larger than 5000px such as 100 x 50
397-
const showBlurPlaceholder =
398-
placeholder === 'blur' && (widthInt || 0) * (heightInt || 0) > 5000
399-
400415
let wrapperStyle: JSX.IntrinsicElements['div']['style'] | undefined
401416
let sizerStyle: JSX.IntrinsicElements['div']['style'] | undefined
402417
let sizerSvg: string | undefined
@@ -423,7 +438,7 @@ export default function Image({
423438
objectFit,
424439
objectPosition,
425440

426-
...(showBlurPlaceholder
441+
...(placeholder === 'blur'
427442
? {
428443
filter: 'blur(20px)',
429444
backgroundSize: 'cover',

test/integration/image-component/default/pages/blurry-placeholder.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export default function Page() {
88
<Image
99
priority
1010
id="blurry-placeholder"
11-
src="/test.jpg"
11+
src="/test.ico"
1212
width="400"
1313
height="400"
1414
placeholder="blur"
@@ -19,7 +19,7 @@ export default function Page() {
1919

2020
<Image
2121
id="blurry-placeholder-with-lazy"
22-
src="/test.jpg"
22+
src="/test.bmp"
2323
width="400"
2424
height="400"
2525
placeholder="blur"
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import React from 'react'
2+
import Image from 'next/image'
3+
import testBMP from '../public/test.bmp'
4+
5+
const Page = () => {
6+
return (
7+
<div>
8+
<Image
9+
id="invalid-placeholder-blur-static"
10+
src={testBMP}
11+
placeholder="blur"
12+
/>
13+
</div>
14+
)
15+
}
16+
17+
export default Page
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import React from 'react'
2+
import Image from 'next/image'
3+
4+
const Page = () => {
5+
return (
6+
<div>
7+
<Image id="invalid-placeholder-blur" src="/test.png" placeholder="blur" />
8+
</div>
9+
)
10+
}
11+
12+
export default Page
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import React from 'react'
2+
import Image from 'next/image'
3+
import Small from '../public/small.jpg'
4+
5+
const Page = () => {
6+
return (
7+
<div>
8+
<Image id="small-img-import" src={Small} placeholder="blur" />
9+
</div>
10+
)
11+
}
12+
13+
export default Page

test/integration/image-component/default/pages/static.js

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,13 @@ import Image from 'next/image'
44

55
import testJPG from '../public/test.jpg'
66
import testPNG from '../public/test.png'
7+
import testWEBP from '../public/test.webp'
78
import testSVG from '../public/test.svg'
89
import testGIF from '../public/test.gif'
910
import testBMP from '../public/test.bmp'
1011
import testICO from '../public/test.ico'
11-
import testWEBP from '../public/test.webp'
1212

1313
import TallImage from '../components/TallImage'
14-
const testFiles = [
15-
testJPG,
16-
testPNG,
17-
testSVG,
18-
testGIF,
19-
testBMP,
20-
testICO,
21-
testWEBP,
22-
]
23-
2414
const Page = () => {
2515
return (
2616
<div>
@@ -46,9 +36,14 @@ const Page = () => {
4636
width="400"
4737
height="300"
4838
/>
49-
{testFiles.map((f, i) => (
50-
<Image id={`format-test-${i}`} key={i} src={f} placeholder="blur" />
51-
))}
39+
<br />
40+
<Image id="blur-png" src={testPNG} placeholder="blur" />
41+
<Image id="blur-jpg" src={testJPG} placeholder="blur" />
42+
<Image id="blur-webp" src={testWEBP} placeholder="blur" />
43+
<Image id="static-svg" src={testSVG} />
44+
<Image id="static-gif" src={testGIF} />
45+
<Image id="static-bmp" src={testBMP} />
46+
<Image id="static-ico" src={testICO} />
5247
</div>
5348
)
5449
}
439 Bytes
Loading

test/integration/image-component/default/test/index.test.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,39 @@ function runTests(mode) {
484484
'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://)'
485485
)
486486
})
487+
488+
it('should show error when string src and placeholder=blur and blurDataURL is missing', async () => {
489+
const browser = await webdriver(appPort, '/invalid-placeholder-blur')
490+
491+
expect(await hasRedbox(browser)).toBe(true)
492+
expect(await getRedboxHeader(browser)).toContain(
493+
`Image with src "/test.png" has "placeholder='blur'" property but is missing the "blurDataURL" property.`
494+
)
495+
})
496+
497+
it('should show error when static import and placeholder=blur and blurDataUrl is missing', async () => {
498+
const browser = await webdriver(
499+
appPort,
500+
'/invalid-placeholder-blur-static'
501+
)
502+
503+
expect(await hasRedbox(browser)).toBe(true)
504+
expect(await getRedboxHeader(browser)).toMatch(
505+
/Image with src "(.*)bmp" has "placeholder='blur'" property but is missing the "blurDataURL" property/
506+
)
507+
})
508+
509+
it('should warn when using a very small image with placeholder=blur', async () => {
510+
const browser = await webdriver(appPort, '/small-img-import')
511+
512+
const warnings = (await browser.log('browser'))
513+
.map((log) => log.message)
514+
.join('\n')
515+
expect(await hasRedbox(browser)).toBe(false)
516+
expect(warnings).toMatch(
517+
/Image with src (.*)jpg(.*) is smaller than 40x40. Consider removing(.*)/gm
518+
)
519+
})
487520
}
488521

489522
it('should correctly ignore prose styles', async () => {

0 commit comments

Comments
 (0)