diff --git a/bench/blur-image-benchmark.js b/bench/blur-image-benchmark.js new file mode 100644 index 00000000000000..d96eea487994c7 --- /dev/null +++ b/bench/blur-image-benchmark.js @@ -0,0 +1,254 @@ +// Benchmark: getBlurImage() - original format vs webp conversion +// +// Usage: node bench/blur-image-benchmark.js +// +// Compares latency and blurDataURL size when keeping the original image +// format versus converting to webp for the blur placeholder. + +const path = require('path') +const fs = require('fs') +const { + optimizeImage, +} = require('../packages/next/dist/server/image-optimizer') + +const BLUR_IMG_SIZE = 8 +const BLUR_QUALITY = 70 + +const TEST_IMAGES = [ + // PNG images + { + path: 'test/unit/image-optimizer/images/test.png', + ext: 'png', + width: 400, + height: 400, + }, + { + path: 'test/integration/image-optimizer/app/public/grayscale.png', + ext: 'png', + width: 36, + height: 36, + }, + // JPEG images + { + path: 'test/unit/image-optimizer/images/test.jpg', + ext: 'jpeg', + width: 400, + height: 400, + }, + { + path: 'test/integration/image-optimizer/app/public/mountains.jpg', + ext: 'jpeg', + width: 2800, + height: 1900, + }, + // WebP image (baseline - already webp) + { + path: 'test/unit/image-optimizer/app/public/test.webp', + ext: 'webp', + width: 400, + height: 400, + }, + // Wide PNG images + { + path: 'test/integration/next-image-new/app-dir/public/wide.png', + ext: 'png', + width: 1200, + height: 700, + }, + { + path: 'test/integration/next-image-new/app-dir/public/super-wide.png', + ext: 'png', + width: 1920, + height: 25, + }, + // Photo JPEG images + { + path: 'examples/image-component/public/cat.jpg', + ext: 'jpeg', + width: 1500, + height: 2000, + }, + { + path: 'examples/image-component/public/dog.jpg', + ext: 'jpeg', + width: 1500, + height: 2000, + }, + // Logo PNG + { + path: 'examples/image-component/public/vercel.png', + ext: 'png', + width: 1600, + height: 1600, + }, + // AVIF images + { + path: 'test/unit/image-optimizer/images/test.avif', + ext: 'avif', + width: 400, + height: 400, + }, +] + +function computeBlurDimensions(width, height) { + let blurWidth, blurHeight + if (width >= height) { + blurWidth = BLUR_IMG_SIZE + blurHeight = Math.max(Math.round((height / width) * BLUR_IMG_SIZE), 1) + } else { + blurWidth = Math.max(Math.round((width / height) * BLUR_IMG_SIZE), 1) + blurHeight = BLUR_IMG_SIZE + } + return { blurWidth, blurHeight } +} + +async function runBlur(buffer, ext, blurWidth, blurHeight) { + const optimized = await optimizeImage({ + buffer, + width: blurWidth, + height: blurHeight, + contentType: `image/${ext}`, + quality: BLUR_QUALITY, + }) + const dataURL = `data:image/${ext};base64,${optimized.toString('base64')}` + return { optimized, dataURL } +} + +async function benchmark(label, fn, iterations = 50) { + // Warmup + for (let i = 0; i < 3; i++) { + await fn() + } + + const times = [] + for (let i = 0; i < iterations; i++) { + const start = performance.now() + await fn() + times.push(performance.now() - start) + } + + times.sort((a, b) => a - b) + return { + label, + median: times[Math.floor(times.length / 2)], + mean: times.reduce((a, b) => a + b, 0) / times.length, + p95: times[Math.floor(times.length * 0.95)], + min: times[0], + max: times[times.length - 1], + } +} + +async function main() { + const repoRoot = path.resolve(__dirname, '..') + const iterations = 50 + + console.log(`Blur Image Benchmark (${iterations} iterations each)`) + console.log('='.repeat(100)) + console.log() + + const results = [] + + for (const img of TEST_IMAGES) { + const fullPath = path.join(repoRoot, img.path) + if (!fs.existsSync(fullPath)) { + console.log(`SKIP: ${img.path} (not found)`) + continue + } + + const content = fs.readFileSync(fullPath) + const fileSize = content.length + const { blurWidth, blurHeight } = computeBlurDimensions( + img.width, + img.height + ) + const basename = path.basename(img.path) + + console.log( + `${basename} (${img.ext}, ${fileSize} bytes, ${img.width}x${img.height} → ${blurWidth}x${blurHeight})` + ) + console.log('-'.repeat(100)) + + // Original format + const origResult = await runBlur(content, img.ext, blurWidth, blurHeight) + const origTiming = await benchmark( + ` ${img.ext} (original)`, + () => runBlur(content, img.ext, blurWidth, blurHeight), + iterations + ) + + // WebP format + const webpResult = await runBlur(content, 'webp', blurWidth, blurHeight) + const webpTiming = await benchmark( + ` webp (converted)`, + () => runBlur(content, 'webp', blurWidth, blurHeight), + iterations + ) + + const origDataURLLen = origResult.dataURL.length + const webpDataURLLen = webpResult.dataURL.length + const sizeDiff = ( + ((webpDataURLLen - origDataURLLen) / origDataURLLen) * + 100 + ).toFixed(1) + const speedDiff = ( + ((webpTiming.median - origTiming.median) / origTiming.median) * + 100 + ).toFixed(1) + + console.log( + ` ${'Format'.padEnd(18)} ${'Median (ms)'.padStart(12)} ${'Mean (ms)'.padStart(12)} ${'P95 (ms)'.padStart(12)} ${'DataURL len'.padStart(12)} ${'Buf size'.padStart(10)}` + ) + console.log( + ` ${(img.ext + ' (original)').padEnd(18)} ${origTiming.median.toFixed(2).padStart(12)} ${origTiming.mean.toFixed(2).padStart(12)} ${origTiming.p95.toFixed(2).padStart(12)} ${String(origDataURLLen).padStart(12)} ${String(origResult.optimized.length).padStart(10)}` + ) + console.log( + ` ${'webp (converted)'.padEnd(18)} ${webpTiming.median.toFixed(2).padStart(12)} ${webpTiming.mean.toFixed(2).padStart(12)} ${webpTiming.p95.toFixed(2).padStart(12)} ${String(webpDataURLLen).padStart(12)} ${String(webpResult.optimized.length).padStart(10)}` + ) + console.log( + ` Δ webp vs orig: speed ${speedDiff}% | dataURL size ${sizeDiff}%` + ) + console.log() + + results.push({ + file: basename, + ext: img.ext, + fileSize, + origMedianMs: origTiming.median, + webpMedianMs: webpTiming.median, + origDataURLLen, + webpDataURLLen, + origBufSize: origResult.optimized.length, + webpBufSize: webpResult.optimized.length, + }) + } + + // Summary table + console.log() + console.log('SUMMARY') + console.log('='.repeat(100)) + console.log( + `${'File'.padEnd(25)} ${'Ext'.padEnd(6)} ${'Orig ms'.padStart(9)} ${'WebP ms'.padStart(9)} ${'Δ Speed'.padStart(9)} ${'Orig URL'.padStart(10)} ${'WebP URL'.padStart(10)} ${'Δ Size'.padStart(9)}` + ) + console.log('-'.repeat(100)) + + for (const r of results) { + const speedDiff = ( + ((r.webpMedianMs - r.origMedianMs) / r.origMedianMs) * + 100 + ).toFixed(1) + const sizeDiff = ( + ((r.webpDataURLLen - r.origDataURLLen) / r.origDataURLLen) * + 100 + ).toFixed(1) + + console.log( + `${r.file.padEnd(25)} ${r.ext.padEnd(6)} ${r.origMedianMs.toFixed(2).padStart(9)} ${r.webpMedianMs.toFixed(2).padStart(9)} ${(speedDiff + '%').padStart(9)} ${String(r.origDataURLLen).padStart(10)} ${String(r.webpDataURLLen).padStart(10)} ${(sizeDiff + '%').padStart(9)}` + ) + } + console.log() +} + +main().catch((err) => { + console.error(err) + process.exit(1) +}) diff --git a/packages/next/src/build/webpack/loaders/next-image-loader/blur.ts b/packages/next/src/build/webpack/loaders/next-image-loader/blur.ts index b7dbb25086250b..3cf57cd21b6506 100644 --- a/packages/next/src/build/webpack/loaders/next-image-loader/blur.ts +++ b/packages/next/src/build/webpack/loaders/next-image-loader/blur.ts @@ -65,19 +65,21 @@ export async function getBlurImage( blurDataURL = url.href.slice(prefix.length) } else { const resizeImageSpan = tracing('image-resize') + // Use webp placeholder since its fewer bytes and faster to encode + const blurContentType = 'image/webp' const resizedImage = await resizeImageSpan.traceAsyncFn(() => optimizeImage({ buffer: content, width: blurWidth, height: blurHeight, - contentType: `image/${extension}`, + contentType: blurContentType, quality: BLUR_QUALITY, }) ) const blurDataURLSpan = tracing('image-base64-tostring') blurDataURL = blurDataURLSpan.traceFn( () => - `data:image/${extension};base64,${resizedImage.toString('base64')}` + `data:${blurContentType};base64,${resizedImage.toString('base64')}` ) } } diff --git a/packages/next/src/server/image-optimizer.ts b/packages/next/src/server/image-optimizer.ts index 010a88b2f0e72b..34f8292e0f0643 100644 --- a/packages/next/src/server/image-optimizer.ts +++ b/packages/next/src/server/image-optimizer.ts @@ -1142,9 +1142,13 @@ export async function imageOptimizer( } try { + const isBlur = + opts.isDev && width <= BLUR_IMG_SIZE && quality === BLUR_QUALITY + // Use webp placeholder since its fewer bytes and faster to encode + const blurContentType = isBlur ? WEBP : contentType let optimizedBuffer = await optimizeImage({ buffer: upstreamBuffer, - contentType, + contentType: blurContentType, quality, width, concurrency: nextConfig.experimental.imgOptConcurrency, @@ -1152,7 +1156,7 @@ export async function imageOptimizer( sequentialRead: nextConfig.experimental.imgOptSequentialRead, timeoutInSeconds: nextConfig.experimental.imgOptTimeoutInSeconds, }) - if (opts.isDev && width <= BLUR_IMG_SIZE && quality === BLUR_QUALITY) { + if (isBlur) { // During `next dev`, we don't want to generate blur placeholders with webpack // because it can delay starting the dev server. Instead, `next-image-loader.js` // will inline a special url to lazily generate the blur placeholder at request time. @@ -1160,7 +1164,7 @@ export async function imageOptimizer( const blurOpts = { blurWidth: meta.width, blurHeight: meta.height, - blurDataURL: `data:${contentType};base64,${optimizedBuffer.toString( + blurDataURL: `data:${blurContentType};base64,${optimizedBuffer.toString( 'base64' )}`, } diff --git a/test/integration/next-image-legacy/asset-prefix/test/index.test.ts b/test/integration/next-image-legacy/asset-prefix/test/index.test.ts index 1aaa224f52d01a..577a1b925917f6 100644 --- a/test/integration/next-image-legacy/asset-prefix/test/index.test.ts +++ b/test/integration/next-image-legacy/asset-prefix/test/index.test.ts @@ -34,7 +34,7 @@ describe('Image Component assetPrefix Tests', () => { `document.getElementById('${id}').style['background-image']` ) if (process.env.IS_TURBOPACK_TEST) { - expect(bgImage).toContain('data:image/jpeg;') + expect(bgImage).toContain('data:image/webp;') } else { expect(bgImage).toMatch( /\/_next\/image\?url=https%3A%2F%2Fexample.com%2Fpre%2F_next%2Fstatic%2Fmedia%2Ftest(.+).jpg&w=8&q=70/ @@ -66,7 +66,7 @@ describe('Image Component assetPrefix Tests', () => { const bgImage = await browser.eval( `document.getElementById('${id}').style['background-image']` ) - expect(bgImage).toMatch('data:image/jpeg;base64') + expect(bgImage).toMatch('data:image/webp;base64') } finally { if (browser) { await browser.close() diff --git a/test/integration/next-image-legacy/base-path/test/static.test.ts b/test/integration/next-image-legacy/base-path/test/static.test.ts index 2311fa3abcfd29..08f683a3515f14 100644 --- a/test/integration/next-image-legacy/base-path/test/static.test.ts +++ b/test/integration/next-image-legacy/base-path/test/static.test.ts @@ -71,14 +71,14 @@ const runTests = (isDev = false) => { it('Should add a blur placeholder to statically imported jpg', async () => { if (process.env.IS_TURBOPACK_TEST) { expect(html).toContain( - `style="position:absolute;top:0;left:0;bottom:0;right:0;box-sizing:border-box;padding:0;border:none;margin:auto;display:block;width:0;height:0;min-width:100%;max-width:100%;min-height:100%;max-height:100%;background-size:cover;background-position:0% 0%;filter:blur(20px);background-image:url("data:image/jpeg;base64` + `style="position:absolute;top:0;left:0;bottom:0;right:0;box-sizing:border-box;padding:0;border:none;margin:auto;display:block;width:0;height:0;min-width:100%;max-width:100%;min-height:100%;max-height:100%;background-size:cover;background-position:0% 0%;filter:blur(20px);background-image:url("data:image/webp;base64` ) } else { expect(html).toContain( `style="position:absolute;top:0;left:0;bottom:0;right:0;box-sizing:border-box;padding:0;border:none;margin:auto;display:block;width:0;height:0;min-width:100%;max-width:100%;min-height:100%;max-height:100%;background-size:cover;background-position:0% 0%;filter:blur(20px);background-image:url(${ isDev ? '"/docs/_next/image?url=%2Fdocs%2F_next%2Fstatic%2Fmedia%2Ftest.fab2915d.jpg&w=8&q=70"' - : '"data:image/jpeg;base64,/9j/2wBDAAoKCgoKCgsMDAsPEA4QDxYUExMUFiIYGhgaGCIzICUgICUgMy03LCksNy1RQDg4QFFeT0pPXnFlZXGPiI+7u/v/2wBDAQoKCgoKCgsMDAsPEA4QDxYUExMUFiIYGhgaGCIzICUgICUgMy03LCksNy1RQDg4QFFeT0pPXnFlZXGPiI+7u/v/wgARCAAGAAgDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAf/xAAUAQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIQAxAAAACUg//EABwQAAICAgMAAAAAAAAAAAAAABITERQAAwUVIv/aAAgBAQABPwB3H9YmrsuvN5+VxADn/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAgBAgEBPwB//8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAgBAwEBPwB//9k="' + : '"data:image/webp;base64,UklGRkgAAABXRUJQVlA4IDwAAADQAQCdASoIAAYAAkA4JaQAAv3PE8XgAAD+/A/T3X/65PagU4f97MeqEqvtHB2jiki/Oa5v/xElxfAAAAA="' })` ) } @@ -86,14 +86,14 @@ const runTests = (isDev = false) => { it('Should add a blur placeholder to statically imported png', async () => { if (process.env.IS_TURBOPACK_TEST) { expect(html).toContain( - `style="position:absolute;top:0;left:0;bottom:0;right:0;box-sizing:border-box;padding:0;border:none;margin:auto;display:block;width:0;height:0;min-width:100%;max-width:100%;min-height:100%;max-height:100%;background-size:cover;background-position:0% 0%;filter:blur(20px);background-image:url("data:image/png;base64` + `style="position:absolute;top:0;left:0;bottom:0;right:0;box-sizing:border-box;padding:0;border:none;margin:auto;display:block;width:0;height:0;min-width:100%;max-width:100%;min-height:100%;max-height:100%;background-size:cover;background-position:0% 0%;filter:blur(20px);background-image:url("data:image/webp;base64` ) } else { expect(html).toContain( `style="position:absolute;top:0;left:0;bottom:0;right:0;box-sizing:border-box;padding:0;border:none;margin:auto;display:block;width:0;height:0;min-width:100%;max-width:100%;min-height:100%;max-height:100%;background-size:cover;background-position:0% 0%;filter:blur(20px);background-image:url(${ isDev ? '"/docs/_next/image?url=%2Fdocs%2F_next%2Fstatic%2Fmedia%2Ftest.3f1a293b.png&w=8&q=70"' - : '"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAMAAADz0U65AAAAElBMVEUAAAA6OjolJSWwsLAfHx/9/f2oxsg2AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAH0lEQVR4nGNgwAaYmKAMZmYIzcjKyghmsDAysmDTAgAEXAAhXbseDQAAAABJRU5ErkJggg=="' + : '"data:image/webp;base64,UklGRk4AAABXRUJQVlA4IEIAAADwAQCdASoIAAgAAkA4JaQAD4APv4EDcgAA/v32WXEBPbC7La/JYB9bEMPATzP6MRoo7uZzEV+7P1I2RH6Q0VVUAAA="' })` ) } diff --git a/test/integration/next-image-legacy/default/test/static.test.ts b/test/integration/next-image-legacy/default/test/static.test.ts index 52a3615174f733..52c7e80aa21d8f 100644 --- a/test/integration/next-image-legacy/default/test/static.test.ts +++ b/test/integration/next-image-legacy/default/test/static.test.ts @@ -86,11 +86,11 @@ const runTests = () => { const $ = cheerio.load(html) if (process.env.IS_TURBOPACK_TEST) { expect($('#basic-static')[2].attribs.style).toMatchInlineSnapshot( - `"position:absolute;top:0;left:0;bottom:0;right:0;box-sizing:border-box;padding:0;border:none;margin:auto;display:block;width:0;height:0;min-width:100%;max-width:100%;min-height:100%;max-height:100%;background-size:cover;background-position:0% 0%;filter:blur(20px);background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAICAYAAAA870V8AAAARUlEQVR42l3MoQ0AQQhE0XG7xWwIJSBIKBRJOZRBEXOWnPjimQ8AXC3ce+nuPOcQEcHuppkRVcWZYWYSIkJV5XvvN9j4AFZHJTnjDHb/AAAAAElFTkSuQmCC")"` + `"position:absolute;top:0;left:0;bottom:0;right:0;box-sizing:border-box;padding:0;border:none;margin:auto;display:block;width:0;height:0;min-width:100%;max-width:100%;min-height:100%;max-height:100%;background-size:cover;background-position:0% 0%;filter:blur(20px);background-image:url("data:image/webp;base64,UklGRlAAAABXRUJQVlA4TEQAAAAvAsABEM1VICICHggACQAAAICAAwAEAAAAMAgKAAAAFAAAABAIBAAAAAAAAACwBQAAAAAAEQAAIiJhoRyu62Sh+V/DAA==")"` ) } else { expect($('#basic-static')[2].attribs.style).toMatchInlineSnapshot( - `"position:absolute;top:0;left:0;bottom:0;right:0;box-sizing:border-box;padding:0;border:none;margin:auto;display:block;width:0;height:0;min-width:100%;max-width:100%;min-height:100%;max-height:100%;background-size:cover;background-position:0% 0%;filter:blur(20px);background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAICAMAAAALMbVOAAAAGFBMVEUBAQFCQkIHBwcuLi79/f0rKyu1tbWurq7lN1wyAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAHElEQVR4nGNggAImRiYGRhZGBjYWdgZmVmaYMAACVQAo1/LzagAAAABJRU5ErkJggg==")"` + `"position:absolute;top:0;left:0;bottom:0;right:0;box-sizing:border-box;padding:0;border:none;margin:auto;display:block;width:0;height:0;min-width:100%;max-width:100%;min-height:100%;max-height:100%;background-size:cover;background-position:0% 0%;filter:blur(20px);background-image:url("data:image/webp;base64,UklGRj4AAABXRUJQVlA4IDIAAACwAQCdASoDAAgAAkA4JaQAAueAEf5AAP78B6F8mD3u+drS6q6DCrFdMeXByXKzBBsAAA==")"` ) } }) diff --git a/test/integration/next-image-new/app-dir/test/static.test.ts b/test/integration/next-image-new/app-dir/test/static.test.ts index 148e02e0f114b0..b49fe92a40e607 100644 --- a/test/integration/next-image-new/app-dir/test/static.test.ts +++ b/test/integration/next-image-new/app-dir/test/static.test.ts @@ -144,7 +144,7 @@ const runTests = (isDev) => { ) } else { expect(style).toBe( - `color:transparent;background-size:cover;background-position:50% 50%;background-repeat:no-repeat;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 320 240'%3E%3Cfilter id='b' color-interpolation-filters='sRGB'%3E%3CfeGaussianBlur stdDeviation='20'/%3E%3CfeColorMatrix values='1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 100 -1' result='s'/%3E%3CfeFlood x='0' y='0' width='100%25' height='100%25'/%3E%3CfeComposite operator='out' in='s'/%3E%3CfeComposite in2='SourceGraphic'/%3E%3CfeGaussianBlur stdDeviation='20'/%3E%3C/filter%3E%3Cimage width='100%25' height='100%25' x='0' y='0' preserveAspectRatio='none' style='filter: url(%23b);' href='data:image/jpeg;base64,/9j/2wBDAAoKCgoKCgsMDAsPEA4QDxYUExMUFiIYGhgaGCIzICUgICUgMy03LCksNy1RQDg4QFFeT0pPXnFlZXGPiI+7u/v/2wBDAQoKCgoKCgsMDAsPEA4QDxYUExMUFiIYGhgaGCIzICUgICUgMy03LCksNy1RQDg4QFFeT0pPXnFlZXGPiI+7u/v/wgARCAAGAAgDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAf/xAAUAQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIQAxAAAACUg//EABwQAAICAgMAAAAAAAAAAAAAABITERQAAwUVIv/aAAgBAQABPwB3H9YmrsuvN5+VxADn/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAgBAgEBPwB//8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAgBAwEBPwB//9k='/%3E%3C/svg%3E")` + `color:transparent;background-size:cover;background-position:50% 50%;background-repeat:no-repeat;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 320 240'%3E%3Cfilter id='b' color-interpolation-filters='sRGB'%3E%3CfeGaussianBlur stdDeviation='20'/%3E%3CfeColorMatrix values='1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 100 -1' result='s'/%3E%3CfeFlood x='0' y='0' width='100%25' height='100%25'/%3E%3CfeComposite operator='out' in='s'/%3E%3CfeComposite in2='SourceGraphic'/%3E%3CfeGaussianBlur stdDeviation='20'/%3E%3C/filter%3E%3Cimage width='100%25' height='100%25' x='0' y='0' preserveAspectRatio='none' style='filter: url(%23b);' href='data:image/webp;base64,UklGRkgAAABXRUJQVlA4IDwAAADQAQCdASoIAAYAAkA4JaQAAv3PE8XgAAD+/A/T3X/65PagU4f97MeqEqvtHB2jiki/Oa5v/xElxfAAAAA='/%3E%3C/svg%3E")` ) } } @@ -169,7 +169,7 @@ const runTests = (isDev) => { ) } else { expect(style).toBe( - `color:transparent;background-size:cover;background-position:50% 50%;background-repeat:no-repeat;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 320 320'%3E%3Cfilter id='b' color-interpolation-filters='sRGB'%3E%3CfeGaussianBlur stdDeviation='20'/%3E%3CfeColorMatrix values='1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 100 -1' result='s'/%3E%3CfeFlood x='0' y='0' width='100%25' height='100%25'/%3E%3CfeComposite operator='out' in='s'/%3E%3CfeComposite in2='SourceGraphic'/%3E%3CfeGaussianBlur stdDeviation='20'/%3E%3C/filter%3E%3Cimage width='100%25' height='100%25' x='0' y='0' preserveAspectRatio='none' style='filter: url(%23b);' href='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAMAAADz0U65AAAAElBMVEUAAAA6OjolJSWwsLAfHx/9/f2oxsg2AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAH0lEQVR4nGNgwAaYmKAMZmYIzcjKyghmsDAysmDTAgAEXAAhXbseDQAAAABJRU5ErkJggg=='/%3E%3C/svg%3E")` + `color:transparent;background-size:cover;background-position:50% 50%;background-repeat:no-repeat;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 320 320'%3E%3Cfilter id='b' color-interpolation-filters='sRGB'%3E%3CfeGaussianBlur stdDeviation='20'/%3E%3CfeColorMatrix values='1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 100 -1' result='s'/%3E%3CfeFlood x='0' y='0' width='100%25' height='100%25'/%3E%3CfeComposite operator='out' in='s'/%3E%3CfeComposite in2='SourceGraphic'/%3E%3CfeGaussianBlur stdDeviation='20'/%3E%3C/filter%3E%3Cimage width='100%25' height='100%25' x='0' y='0' preserveAspectRatio='none' style='filter: url(%23b);' href='data:image/webp;base64,UklGRk4AAABXRUJQVlA4IEIAAADwAQCdASoIAAgAAkA4JaQAD4APv4EDcgAA/v32WXEBPbC7La/JYB9bEMPATzP6MRoo7uZzEV+7P1I2RH6Q0VVUAAA='/%3E%3C/svg%3E")` ) } } @@ -194,7 +194,7 @@ const runTests = (isDev) => { ) } else { expect(style).toBe( - `position:absolute;height:100%;width:100%;left:0;top:0;right:0;bottom:0;color:transparent;background-size:cover;background-position:50% 50%;background-repeat:no-repeat;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 320 320'%3E%3Cfilter id='b' color-interpolation-filters='sRGB'%3E%3CfeGaussianBlur stdDeviation='20'/%3E%3CfeColorMatrix values='1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 100 -1' result='s'/%3E%3CfeFlood x='0' y='0' width='100%25' height='100%25'/%3E%3CfeComposite operator='out' in='s'/%3E%3CfeComposite in2='SourceGraphic'/%3E%3CfeGaussianBlur stdDeviation='20'/%3E%3C/filter%3E%3Cimage width='100%25' height='100%25' x='0' y='0' preserveAspectRatio='none' style='filter: url(%23b);' href='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAMAAADz0U65AAAAElBMVEUAAAA6OjolJSWwsLAfHx/9/f2oxsg2AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAH0lEQVR4nGNgwAaYmKAMZmYIzcjKyghmsDAysmDTAgAEXAAhXbseDQAAAABJRU5ErkJggg=='/%3E%3C/svg%3E")` + `position:absolute;height:100%;width:100%;left:0;top:0;right:0;bottom:0;color:transparent;background-size:cover;background-position:50% 50%;background-repeat:no-repeat;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 320 320'%3E%3Cfilter id='b' color-interpolation-filters='sRGB'%3E%3CfeGaussianBlur stdDeviation='20'/%3E%3CfeColorMatrix values='1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 100 -1' result='s'/%3E%3CfeFlood x='0' y='0' width='100%25' height='100%25'/%3E%3CfeComposite operator='out' in='s'/%3E%3CfeComposite in2='SourceGraphic'/%3E%3CfeGaussianBlur stdDeviation='20'/%3E%3C/filter%3E%3Cimage width='100%25' height='100%25' x='0' y='0' preserveAspectRatio='none' style='filter: url(%23b);' href='data:image/webp;base64,UklGRk4AAABXRUJQVlA4IEIAAADwAQCdASoIAAgAAkA4JaQAD4APv4EDcgAA/v32WXEBPbC7La/JYB9bEMPATzP6MRoo7uZzEV+7P1I2RH6Q0VVUAAA='/%3E%3C/svg%3E")` ) } } diff --git a/test/integration/next-image-new/asset-prefix/test/index.test.ts b/test/integration/next-image-new/asset-prefix/test/index.test.ts index fb71088d257ca9..02e997e0360f61 100644 --- a/test/integration/next-image-new/asset-prefix/test/index.test.ts +++ b/test/integration/next-image-new/asset-prefix/test/index.test.ts @@ -84,7 +84,7 @@ describe('Image Component assetPrefix Tests', () => { const bgImage = await browser.eval( `document.getElementById('${id}').style['background-image']` ) - expect(bgImage).toMatch('data:image/jpeg;base64') + expect(bgImage).toMatch('data:image/webp;base64') }) // eslint-disable-next-line jest/no-identical-title diff --git a/test/integration/next-image-new/base-path/test/static.test.ts b/test/integration/next-image-new/base-path/test/static.test.ts index 531f99be334221..eb654aee83a750 100644 --- a/test/integration/next-image-new/base-path/test/static.test.ts +++ b/test/integration/next-image-new/base-path/test/static.test.ts @@ -137,7 +137,7 @@ const runTests = (isDev) => { ) } else { expect(style).toBe( - `color:transparent;background-size:cover;background-position:50% 50%;background-repeat:no-repeat;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 320 240'%3E%3Cfilter id='b' color-interpolation-filters='sRGB'%3E%3CfeGaussianBlur stdDeviation='20'/%3E%3CfeColorMatrix values='1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 100 -1' result='s'/%3E%3CfeFlood x='0' y='0' width='100%25' height='100%25'/%3E%3CfeComposite operator='out' in='s'/%3E%3CfeComposite in2='SourceGraphic'/%3E%3CfeGaussianBlur stdDeviation='20'/%3E%3C/filter%3E%3Cimage width='100%25' height='100%25' x='0' y='0' preserveAspectRatio='none' style='filter: url(%23b);' href='data:image/jpeg;base64,/9j/2wBDAAoKCgoKCgsMDAsPEA4QDxYUExMUFiIYGhgaGCIzICUgICUgMy03LCksNy1RQDg4QFFeT0pPXnFlZXGPiI+7u/v/2wBDAQoKCgoKCgsMDAsPEA4QDxYUExMUFiIYGhgaGCIzICUgICUgMy03LCksNy1RQDg4QFFeT0pPXnFlZXGPiI+7u/v/wgARCAAGAAgDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAf/xAAUAQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIQAxAAAACUg//EABwQAAICAgMAAAAAAAAAAAAAABITERQAAwUVIv/aAAgBAQABPwB3H9YmrsuvN5+VxADn/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAgBAgEBPwB//8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAgBAwEBPwB//9k='/%3E%3C/svg%3E")` + `color:transparent;background-size:cover;background-position:50% 50%;background-repeat:no-repeat;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 320 240'%3E%3Cfilter id='b' color-interpolation-filters='sRGB'%3E%3CfeGaussianBlur stdDeviation='20'/%3E%3CfeColorMatrix values='1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 100 -1' result='s'/%3E%3CfeFlood x='0' y='0' width='100%25' height='100%25'/%3E%3CfeComposite operator='out' in='s'/%3E%3CfeComposite in2='SourceGraphic'/%3E%3CfeGaussianBlur stdDeviation='20'/%3E%3C/filter%3E%3Cimage width='100%25' height='100%25' x='0' y='0' preserveAspectRatio='none' style='filter: url(%23b);' href='data:image/webp;base64,UklGRkgAAABXRUJQVlA4IDwAAADQAQCdASoIAAYAAkA4JaQAAv3PE8XgAAD+/A/T3X/65PagU4f97MeqEqvtHB2jiki/Oa5v/xElxfAAAAA='/%3E%3C/svg%3E")` ) } } @@ -162,7 +162,7 @@ const runTests = (isDev) => { ) } else { expect(style).toBe( - `color:transparent;background-size:cover;background-position:50% 50%;background-repeat:no-repeat;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 320 320'%3E%3Cfilter id='b' color-interpolation-filters='sRGB'%3E%3CfeGaussianBlur stdDeviation='20'/%3E%3CfeColorMatrix values='1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 100 -1' result='s'/%3E%3CfeFlood x='0' y='0' width='100%25' height='100%25'/%3E%3CfeComposite operator='out' in='s'/%3E%3CfeComposite in2='SourceGraphic'/%3E%3CfeGaussianBlur stdDeviation='20'/%3E%3C/filter%3E%3Cimage width='100%25' height='100%25' x='0' y='0' preserveAspectRatio='none' style='filter: url(%23b);' href='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAMAAADz0U65AAAAElBMVEUAAAA6OjolJSWwsLAfHx/9/f2oxsg2AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAH0lEQVR4nGNgwAaYmKAMZmYIzcjKyghmsDAysmDTAgAEXAAhXbseDQAAAABJRU5ErkJggg=='/%3E%3C/svg%3E")` + `color:transparent;background-size:cover;background-position:50% 50%;background-repeat:no-repeat;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 320 320'%3E%3Cfilter id='b' color-interpolation-filters='sRGB'%3E%3CfeGaussianBlur stdDeviation='20'/%3E%3CfeColorMatrix values='1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 100 -1' result='s'/%3E%3CfeFlood x='0' y='0' width='100%25' height='100%25'/%3E%3CfeComposite operator='out' in='s'/%3E%3CfeComposite in2='SourceGraphic'/%3E%3CfeGaussianBlur stdDeviation='20'/%3E%3C/filter%3E%3Cimage width='100%25' height='100%25' x='0' y='0' preserveAspectRatio='none' style='filter: url(%23b);' href='data:image/webp;base64,UklGRk4AAABXRUJQVlA4IEIAAADwAQCdASoIAAgAAkA4JaQAD4APv4EDcgAA/v32WXEBPbC7La/JYB9bEMPATzP6MRoo7uZzEV+7P1I2RH6Q0VVUAAA='/%3E%3C/svg%3E")` ) } } diff --git a/test/integration/next-image-new/default/test/static.test.ts b/test/integration/next-image-new/default/test/static.test.ts index 83d40c2f1932de..5a32ccbd378411 100644 --- a/test/integration/next-image-new/default/test/static.test.ts +++ b/test/integration/next-image-new/default/test/static.test.ts @@ -140,7 +140,7 @@ const runTests = (isDev) => { ) } else { expect(style).toBe( - `color:transparent;background-size:cover;background-position:50% 50%;background-repeat:no-repeat;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 320 240'%3E%3Cfilter id='b' color-interpolation-filters='sRGB'%3E%3CfeGaussianBlur stdDeviation='20'/%3E%3CfeColorMatrix values='1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 100 -1' result='s'/%3E%3CfeFlood x='0' y='0' width='100%25' height='100%25'/%3E%3CfeComposite operator='out' in='s'/%3E%3CfeComposite in2='SourceGraphic'/%3E%3CfeGaussianBlur stdDeviation='20'/%3E%3C/filter%3E%3Cimage width='100%25' height='100%25' x='0' y='0' preserveAspectRatio='none' style='filter: url(%23b);' href='data:image/jpeg;base64,/9j/2wBDAAoKCgoKCgsMDAsPEA4QDxYUExMUFiIYGhgaGCIzICUgICUgMy03LCksNy1RQDg4QFFeT0pPXnFlZXGPiI+7u/v/2wBDAQoKCgoKCgsMDAsPEA4QDxYUExMUFiIYGhgaGCIzICUgICUgMy03LCksNy1RQDg4QFFeT0pPXnFlZXGPiI+7u/v/wgARCAAGAAgDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAf/xAAUAQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIQAxAAAACUg//EABwQAAICAgMAAAAAAAAAAAAAABITERQAAwUVIv/aAAgBAQABPwB3H9YmrsuvN5+VxADn/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAgBAgEBPwB//8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAgBAwEBPwB//9k='/%3E%3C/svg%3E")` + `color:transparent;background-size:cover;background-position:50% 50%;background-repeat:no-repeat;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 320 240'%3E%3Cfilter id='b' color-interpolation-filters='sRGB'%3E%3CfeGaussianBlur stdDeviation='20'/%3E%3CfeColorMatrix values='1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 100 -1' result='s'/%3E%3CfeFlood x='0' y='0' width='100%25' height='100%25'/%3E%3CfeComposite operator='out' in='s'/%3E%3CfeComposite in2='SourceGraphic'/%3E%3CfeGaussianBlur stdDeviation='20'/%3E%3C/filter%3E%3Cimage width='100%25' height='100%25' x='0' y='0' preserveAspectRatio='none' style='filter: url(%23b);' href='data:image/webp;base64,UklGRkgAAABXRUJQVlA4IDwAAADQAQCdASoIAAYAAkA4JaQAAv3PE8XgAAD+/A/T3X/65PagU4f97MeqEqvtHB2jiki/Oa5v/xElxfAAAAA='/%3E%3C/svg%3E")` ) } } @@ -165,7 +165,7 @@ const runTests = (isDev) => { ) } else { expect(style).toBe( - `color:transparent;background-size:cover;background-position:50% 50%;background-repeat:no-repeat;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 320 320'%3E%3Cfilter id='b' color-interpolation-filters='sRGB'%3E%3CfeGaussianBlur stdDeviation='20'/%3E%3CfeColorMatrix values='1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 100 -1' result='s'/%3E%3CfeFlood x='0' y='0' width='100%25' height='100%25'/%3E%3CfeComposite operator='out' in='s'/%3E%3CfeComposite in2='SourceGraphic'/%3E%3CfeGaussianBlur stdDeviation='20'/%3E%3C/filter%3E%3Cimage width='100%25' height='100%25' x='0' y='0' preserveAspectRatio='none' style='filter: url(%23b);' href='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAMAAADz0U65AAAAElBMVEUAAAA6OjolJSWwsLAfHx/9/f2oxsg2AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAH0lEQVR4nGNgwAaYmKAMZmYIzcjKyghmsDAysmDTAgAEXAAhXbseDQAAAABJRU5ErkJggg=='/%3E%3C/svg%3E")` + `color:transparent;background-size:cover;background-position:50% 50%;background-repeat:no-repeat;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 320 320'%3E%3Cfilter id='b' color-interpolation-filters='sRGB'%3E%3CfeGaussianBlur stdDeviation='20'/%3E%3CfeColorMatrix values='1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 100 -1' result='s'/%3E%3CfeFlood x='0' y='0' width='100%25' height='100%25'/%3E%3CfeComposite operator='out' in='s'/%3E%3CfeComposite in2='SourceGraphic'/%3E%3CfeGaussianBlur stdDeviation='20'/%3E%3C/filter%3E%3Cimage width='100%25' height='100%25' x='0' y='0' preserveAspectRatio='none' style='filter: url(%23b);' href='data:image/webp;base64,UklGRk4AAABXRUJQVlA4IEIAAADwAQCdASoIAAgAAkA4JaQAD4APv4EDcgAA/v32WXEBPbC7La/JYB9bEMPATzP6MRoo7uZzEV+7P1I2RH6Q0VVUAAA='/%3E%3C/svg%3E")` ) } } @@ -190,7 +190,7 @@ const runTests = (isDev) => { ) } else { expect(style).toBe( - `position:absolute;height:100%;width:100%;left:0;top:0;right:0;bottom:0;color:transparent;background-size:cover;background-position:50% 50%;background-repeat:no-repeat;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 320 320'%3E%3Cfilter id='b' color-interpolation-filters='sRGB'%3E%3CfeGaussianBlur stdDeviation='20'/%3E%3CfeColorMatrix values='1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 100 -1' result='s'/%3E%3CfeFlood x='0' y='0' width='100%25' height='100%25'/%3E%3CfeComposite operator='out' in='s'/%3E%3CfeComposite in2='SourceGraphic'/%3E%3CfeGaussianBlur stdDeviation='20'/%3E%3C/filter%3E%3Cimage width='100%25' height='100%25' x='0' y='0' preserveAspectRatio='none' style='filter: url(%23b);' href='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAMAAADz0U65AAAAElBMVEUAAAA6OjolJSWwsLAfHx/9/f2oxsg2AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAH0lEQVR4nGNgwAaYmKAMZmYIzcjKyghmsDAysmDTAgAEXAAhXbseDQAAAABJRU5ErkJggg=='/%3E%3C/svg%3E")` + `position:absolute;height:100%;width:100%;left:0;top:0;right:0;bottom:0;color:transparent;background-size:cover;background-position:50% 50%;background-repeat:no-repeat;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 320 320'%3E%3Cfilter id='b' color-interpolation-filters='sRGB'%3E%3CfeGaussianBlur stdDeviation='20'/%3E%3CfeColorMatrix values='1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 100 -1' result='s'/%3E%3CfeFlood x='0' y='0' width='100%25' height='100%25'/%3E%3CfeComposite operator='out' in='s'/%3E%3CfeComposite in2='SourceGraphic'/%3E%3CfeGaussianBlur stdDeviation='20'/%3E%3C/filter%3E%3Cimage width='100%25' height='100%25' x='0' y='0' preserveAspectRatio='none' style='filter: url(%23b);' href='data:image/webp;base64,UklGRk4AAABXRUJQVlA4IEIAAADwAQCdASoIAAgAAkA4JaQAD4APv4EDcgAA/v32WXEBPbC7La/JYB9bEMPATzP6MRoo7uZzEV+7P1I2RH6Q0VVUAAA='/%3E%3C/svg%3E")` ) } } diff --git a/turbopack/crates/turbopack-image/src/process/mod.rs b/turbopack/crates/turbopack-image/src/process/mod.rs index ec021ecc3baa75..8970ccbd4fca74 100644 --- a/turbopack/crates/turbopack-image/src/process/mod.rs +++ b/turbopack/crates/turbopack-image/src/process/mod.rs @@ -302,7 +302,13 @@ fn compute_blur_data_internal( let small_image = image.resize(options.size, options.size, FilterType::Triangle); let width = small_image.width(); let height = small_image.height(); - let (data, mime) = encode_image(small_image, format, options.quality)?; + // Prefer WebP for smaller blur placeholders when available + let blur_format = if cfg!(feature = "webp") { + ImageFormat::WebP + } else { + format + }; + let (data, mime) = encode_image(small_image, blur_format, options.quality)?; let data_url = format!( "data:{mime};base64,{}", Base64Display::new(&data, &STANDARD)