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:
parent
7a8da9741d
commit
31c3f33639
4 changed files with 64 additions and 20 deletions
|
@ -1100,6 +1100,8 @@ export default async function getBaseWebpackConfig(
|
|||
dependency: { not: ['url'] },
|
||||
options: {
|
||||
isServer,
|
||||
isDev: dev,
|
||||
assetPrefix: config.assetPrefix,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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`)
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue