Skip to content

Commit

Permalink
Enhance next dev performance with placeholder=blur (#27061)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
styfle authored Jul 10, 2021
1 parent 7a8da97 commit 31c3f33
Show file tree
Hide file tree
Showing 4 changed files with 63 additions and 19 deletions.
2 changes: 2 additions & 0 deletions packages/next/build/webpack-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1100,6 +1100,8 @@ export default async function getBaseWebpackConfig(
dependency: { not: ['url'] },
options: {
isServer,
isDev: dev,
assetPrefix: config.assetPrefix,
},
},
]
Expand Down
47 changes: 29 additions & 18 deletions packages/next/build/webpack/loaders/next-image-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@ 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(
this,
'/static/image/[path][name].[hash].[ext]',
opts
)
const outputPath = '/_next' + interpolatedName

let extension = loaderUtils.interpolateName(this, '[ext]', opts)
if (extension === 'jpg') {
Expand All @@ -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 }
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 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,
Expand Down
6 changes: 5 additions & 1 deletion packages/next/server/image-optimizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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`)
Expand Down
27 changes: 27 additions & 0 deletions test/integration/image-optimizer/test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
})
})

0 comments on commit 31c3f33

Please sign in to comment.