Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 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,8 @@ 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 VALID_IMAGE_TYPES = ['jpeg', 'png']

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

const imageSize = sizeOf(content)
let placeholder
if (extension === 'jpeg' || extension === 'png') {
let blurDataURL
if (VALID_IMAGE_TYPES.includes(extension)) {
// Shrink the image's largest dimension to 6 pixels
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
)
placeholder = `data:image/${extension};base64,${resizedImage.toString(
blurDataURL = `data:image/${extension};base64,${resizedImage.toString(
'base64'
)}`
}
Expand All @@ -41,7 +42,7 @@ async function nextImageLoader(content) {
src: '/_next' + interpolatedName,
height: imageSize.height,
width: imageSize.width,
placeholder,
blurDataURL,
})

this.emitFile(interpolatedName, content, null)
Expand Down
32 changes: 20 additions & 12 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,8 +329,8 @@ export default function Image({
)}`
)
}
if (staticImageData.placeholder) {
blurDataURL = staticImageData.placeholder
if (staticImageData.blurDataURL) {
blurDataURL = staticImageData.blurDataURL
}
staticSrc = staticImageData.src
if (!layout || layout !== 'fill') {
Expand All @@ -347,6 +347,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 +378,18 @@ 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) {
throw new Error(
`Image with src "${src}" has "placeholder='blur'" property but is missing the "blurDataURL" property.`
)
}
}
}
let isLazy =
!priority && (loading === 'lazy' || typeof loading === 'undefined')
Expand All @@ -389,14 +405,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 +431,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,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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 21 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,27 @@ 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 invalid placeholder when 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 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