Enhance next dev performance with placeholder=blur (#27061)

This PR changes the implementation of `placeholder=blur` when using `next dev` so that it lazy loads on-demand.

This will improve the developer experience for web apps with many blurred images.
This commit is contained in:
Steven 2021-07-10 16:27:14 -04:00 committed by GitHub
parent 7a8da9741d
commit 31c3f33639
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 64 additions and 20 deletions

View file

@ -1100,6 +1100,8 @@ export default async function getBaseWebpackConfig(
dependency: { not: ['url'] },
options: {
isServer,
isDev: dev,
assetPrefix: config.assetPrefix,
},
},
]

View file

@ -9,7 +9,7 @@ const VALID_BLUR_EXT = ['jpeg', 'png', 'webp']
function nextImageLoader(content) {
const imageLoaderSpan = this.currentTraceSpan.traceChild('next-image-loader')
return imageLoaderSpan.traceAsyncFn(async () => {
const isServer = loaderUtils.getOptions(this).isServer
const { isServer, isDev, assetPrefix } = loaderUtils.getOptions(this)
const context = this.rootContext
const opts = { context, content }
const interpolatedName = loaderUtils.interpolateName(
@ -17,6 +17,7 @@ function nextImageLoader(content) {
'/static/image/[path][name].[hash].[ext]',
opts
)
const outputPath = '/_next' + interpolatedName
let extension = loaderUtils.interpolateName(this, '[ext]', opts)
if (extension === 'jpg') {
@ -26,31 +27,41 @@ function nextImageLoader(content) {
const imageSizeSpan = imageLoaderSpan.traceChild('image-size-calculation')
const imageSize = imageSizeSpan.traceFn(() => sizeOf(content))
let blurDataURL
if (VALID_BLUR_EXT.includes(extension)) {
// Shrink the image's largest dimension
const resizeOperationOpts =
imageSize.width >= imageSize.height
? { type: 'resize', width: BLUR_IMG_SIZE }
: { type: 'resize', height: BLUR_IMG_SIZE }
const resizeImageSpan = imageLoaderSpan.traceChild('image-resize')
const resizedImage = await resizeImageSpan.traceAsyncFn(() =>
processBuffer(content, [resizeOperationOpts], extension, BLUR_QUALITY)
)
const blurDataURLSpan = imageLoaderSpan.traceChild(
'image-base64-tostring'
)
blurDataURL = blurDataURLSpan.traceFn(
() =>
`data:image/${extension};base64,${resizedImage.toString('base64')}`
)
if (VALID_BLUR_EXT.includes(extension)) {
if (isDev) {
const prefix = 'http://localhost'
const url = new URL('/_next/image', prefix)
url.searchParams.set('url', assetPrefix + outputPath)
url.searchParams.set('w', BLUR_IMG_SIZE)
url.searchParams.set('q', BLUR_QUALITY)
blurDataURL = url.href.slice(prefix.length)
} else {
// Shrink the image's largest dimension
const resizeOperationOpts =
imageSize.width >= imageSize.height
? { type: 'resize', width: BLUR_IMG_SIZE }
: { type: 'resize', height: BLUR_IMG_SIZE }
const resizeImageSpan = imageLoaderSpan.traceChild('image-resize')
const resizedImage = await resizeImageSpan.traceAsyncFn(() =>
processBuffer(content, [resizeOperationOpts], extension, BLUR_QUALITY)
)
const blurDataURLSpan = imageLoaderSpan.traceChild(
'image-base64-tostring'
)
blurDataURL = blurDataURLSpan.traceFn(
() =>
`data:image/${extension};base64,${resizedImage.toString('base64')}`
)
}
}
const stringifiedData = imageLoaderSpan
.traceChild('image-data-stringify')
.traceFn(() =>
JSON.stringify({
src: '/_next' + interpolatedName,
src: outputPath,
height: imageSize.height,
width: imageSize.width,
blurDataURL,

View file

@ -26,7 +26,7 @@ const CACHE_VERSION = 3
const MODERN_TYPES = [/* AVIF, */ WEBP]
const ANIMATABLE_TYPES = [WEBP, PNG, GIF]
const VECTOR_TYPES = [SVG]
const BLUR_IMG_SIZE = 8 // should match `next-image-loader`
const inflightRequests = new Map<string, Promise<undefined>>()
export async function imageOptimizer(
@ -125,6 +125,10 @@ export async function imageOptimizer(
const sizes = [...deviceSizes, ...imageSizes]
if (isDev) {
sizes.push(BLUR_IMG_SIZE)
}
if (!sizes.includes(width)) {
res.statusCode = 400
res.end(`"w" parameter (width) of ${width} is not allowed`)

View file

@ -932,4 +932,31 @@ describe('Image Optimizer', () => {
await expectWidth(res, 64)
})
})
describe('dev support for dynamic blur placeholder', () => {
beforeAll(async () => {
const json = JSON.stringify({
images: {
deviceSizes: [largeSize],
imageSizes: [],
},
})
nextConfig.replace('{ /* replaceme */ }', json)
appPort = await findPort()
app = await launchApp(appDir, appPort)
})
afterAll(async () => {
await killApp(app)
nextConfig.restore()
await fs.remove(imagesDir)
})
it('should support width 8 per BLUR_IMG_SIZE with next dev', async () => {
const query = { url: '/test.png', w: 8, q: 70 }
const opts = { headers: { accept: 'image/webp' } }
const res = await fetchViaHTTP(appPort, '/_next/image', query, opts)
expect(res.status).toBe(200)
await expectWidth(res, 8)
})
})
})