From 2456f3fc0fa78229adc32a9d3a9e627ee51fbedd Mon Sep 17 00:00:00 2001 From: Steven Date: Thu, 8 Dec 2022 17:11:16 -0600 Subject: [PATCH 1/3] Refactor image optimization util --- ...t-image-loader.js => next-image-loader.ts} | 32 +- packages/next/server/image-optimizer.ts | 278 ++++++++---------- 2 files changed, 147 insertions(+), 163 deletions(-) rename packages/next/build/webpack/loaders/{next-image-loader.js => next-image-loader.ts} (80%) diff --git a/packages/next/build/webpack/loaders/next-image-loader.js b/packages/next/build/webpack/loaders/next-image-loader.ts similarity index 80% rename from packages/next/build/webpack/loaders/next-image-loader.js rename to packages/next/build/webpack/loaders/next-image-loader.ts index 7a24bd9c7b4a0ef..1339ebdb052e8e9 100644 --- a/packages/next/build/webpack/loaders/next-image-loader.js +++ b/packages/next/build/webpack/loaders/next-image-loader.ts @@ -1,14 +1,22 @@ import loaderUtils from 'next/dist/compiled/loader-utils3' -import { resizeImage, getImageSize } from '../../../server/image-optimizer' +import { optimizeImage, getImageSize } from '../../../server/image-optimizer' const BLUR_IMG_SIZE = 8 const BLUR_QUALITY = 70 const VALID_BLUR_EXT = ['jpeg', 'png', 'webp', 'avif'] // should match next/client/image.tsx -function nextImageLoader(content) { +interface Options { + isServer: boolean + isDev: boolean + assetPrefix: string + basePath: string +} + +function nextImageLoader(this: any, content: Buffer) { const imageLoaderSpan = this.currentTraceSpan.traceChild('next-image-loader') return imageLoaderSpan.traceAsyncFn(async () => { - const { isServer, isDev, assetPrefix, basePath } = this.getOptions() + const options: Options = this.getOptions() + const { isServer, isDev, assetPrefix, basePath } = options const context = this.rootContext const opts = { context, content } const interpolatedName = loaderUtils.interpolateName( @@ -33,9 +41,9 @@ function nextImageLoader(content) { throw err } - let blurDataURL - let blurWidth - let blurHeight + let blurDataURL: string + let blurWidth: number + let blurHeight: number if (VALID_BLUR_EXT.includes(extension)) { // Shrink the image's largest dimension @@ -60,13 +68,19 @@ function nextImageLoader(content) { const prefix = 'http://localhost' const url = new URL(`${basePath || ''}/_next/image`, prefix) url.searchParams.set('url', outputPath) - url.searchParams.set('w', blurWidth) - url.searchParams.set('q', BLUR_QUALITY) + url.searchParams.set('w', String(blurWidth)) + url.searchParams.set('q', String(BLUR_QUALITY)) blurDataURL = url.href.slice(prefix.length) } else { const resizeImageSpan = imageLoaderSpan.traceChild('image-resize') const resizedImage = await resizeImageSpan.traceAsyncFn(() => - resizeImage(content, blurWidth, blurHeight, extension, BLUR_QUALITY) + optimizeImage({ + buffer: content, + width: blurWidth, + height: blurHeight, + contentType: 'image/' + extension, + quality: BLUR_QUALITY, + }) ) const blurDataURLSpan = imageLoaderSpan.traceChild( 'image-base64-tostring' diff --git a/packages/next/server/image-optimizer.ts b/packages/next/server/image-optimizer.ts index d0518eb7ab98358..1905fe46f764f40 100644 --- a/packages/next/server/image-optimizer.ts +++ b/packages/next/server/image-optimizer.ts @@ -390,6 +390,123 @@ export function getMaxAge(str: string | null): number { return 0 } +export async function optimizeImage({ + buffer, + contentType, + quality, + width, + height, + nextConfigOutput, +}: { + buffer: Buffer + contentType: string + quality: number + width: number + height?: number + nextConfigOutput?: 'standalone' +}): Promise { + if (isAnimated(buffer)) { + return buffer + } + + let optimizedBuffer = buffer + if (sharp) { + // Begin sharp transformation logic + const transformer = sharp(buffer) + + transformer.rotate() + + if (height) { + transformer.resize(width, height) + } else { + const { width: metaWidth } = await transformer.metadata() + + if (metaWidth && metaWidth > width) { + transformer.resize(width) + } + } + + if (contentType === AVIF) { + if (transformer.avif) { + const avifQuality = quality - 15 + transformer.avif({ + quality: Math.max(avifQuality, 0), + chromaSubsampling: '4:2:0', // same as webp + }) + } else { + console.warn( + chalk.yellow.bold('Warning: ') + + `Your installed version of the 'sharp' package does not support AVIF images. Run 'yarn add sharp@latest' to upgrade to the latest version.\n` + + 'Read more: https://nextjs.org/docs/messages/sharp-version-avif' + ) + transformer.webp({ quality }) + } + } else if (contentType === WEBP) { + transformer.webp({ quality }) + } else if (contentType === PNG) { + transformer.png({ quality }) + } else if (contentType === JPEG) { + transformer.jpeg({ quality }) + } + + optimizedBuffer = await transformer.toBuffer() + // End sharp transformation logic + } else { + if (showSharpMissingWarning && nextConfigOutput) { + // TODO: should we ensure squoosh also works even though we don't + // recommend it be used in production and this is a production feature + console.error( + `Error: 'sharp' is required to be installed in standalone mode for the image optimization to function correctly. Read more at: https://nextjs.org/docs/messages/sharp-missing-in-production` + ) + throw new ImageError(500, 'internal server error') + } + // Show sharp warning in production once + if (showSharpMissingWarning) { + console.warn( + chalk.yellow.bold('Warning: ') + + `For production Image Optimization with Next.js, the optional 'sharp' package is strongly recommended. Run 'yarn add sharp', and Next.js will use it automatically for Image Optimization.\n` + + 'Read more: https://nextjs.org/docs/messages/sharp-missing-in-production' + ) + showSharpMissingWarning = false + } + + // Begin Squoosh transformation logic + const orientation = await getOrientation(buffer) + + const operations: Operation[] = [] + + if (orientation === Orientation.RIGHT_TOP) { + operations.push({ type: 'rotate', numRotations: 1 }) + } else if (orientation === Orientation.BOTTOM_RIGHT) { + operations.push({ type: 'rotate', numRotations: 2 }) + } else if (orientation === Orientation.LEFT_BOTTOM) { + operations.push({ type: 'rotate', numRotations: 3 }) + } else { + // TODO: support more orientations + // eslint-disable-next-line @typescript-eslint/no-unused-vars + // const _: never = orientation + } + + if (height) { + operations.push({ type: 'resize', width, height }) + } else { + operations.push({ type: 'resize', width }) + } + + if (contentType === AVIF) { + optimizedBuffer = await processBuffer(buffer, operations, 'avif', quality) + } else if (contentType === WEBP) { + optimizedBuffer = await processBuffer(buffer, operations, 'webp', quality) + } else if (contentType === PNG) { + optimizedBuffer = await processBuffer(buffer, operations, 'png', quality) + } else if (contentType === JPEG) { + optimizedBuffer = await processBuffer(buffer, operations, 'jpeg', quality) + } + } + + return optimizedBuffer +} + export async function imageOptimizer( _req: IncomingMessage, _res: ServerResponse, @@ -504,114 +621,13 @@ export async function imageOptimizer( contentType = JPEG } try { - let optimizedBuffer: Buffer | undefined - if (sharp) { - // Begin sharp transformation logic - const transformer = sharp(upstreamBuffer) - - transformer.rotate() - - const { width: metaWidth } = await transformer.metadata() - - if (metaWidth && metaWidth > width) { - transformer.resize(width) - } - - if (contentType === AVIF) { - if (transformer.avif) { - const avifQuality = quality - 15 - transformer.avif({ - quality: Math.max(avifQuality, 0), - chromaSubsampling: '4:2:0', // same as webp - }) - } else { - console.warn( - chalk.yellow.bold('Warning: ') + - `Your installed version of the 'sharp' package does not support AVIF images. Run 'yarn add sharp@latest' to upgrade to the latest version.\n` + - 'Read more: https://nextjs.org/docs/messages/sharp-version-avif' - ) - transformer.webp({ quality }) - } - } else if (contentType === WEBP) { - transformer.webp({ quality }) - } else if (contentType === PNG) { - transformer.png({ quality }) - } else if (contentType === JPEG) { - transformer.jpeg({ quality }) - } - - optimizedBuffer = await transformer.toBuffer() - // End sharp transformation logic - } else { - if (showSharpMissingWarning && nextConfig.output === 'standalone') { - // TODO: should we ensure squoosh also works even though we don't - // recommend it be used in production and this is a production feature - console.error( - `Error: 'sharp' is required to be installed in standalone mode for the image optimization to function correctly. Read more at: https://nextjs.org/docs/messages/sharp-missing-in-production` - ) - throw new ImageError(500, 'internal server error') - } - // Show sharp warning in production once - if (showSharpMissingWarning) { - console.warn( - chalk.yellow.bold('Warning: ') + - `For production Image Optimization with Next.js, the optional 'sharp' package is strongly recommended. Run 'yarn add sharp', and Next.js will use it automatically for Image Optimization.\n` + - 'Read more: https://nextjs.org/docs/messages/sharp-missing-in-production' - ) - showSharpMissingWarning = false - } - - // Begin Squoosh transformation logic - const orientation = await getOrientation(upstreamBuffer) - - const operations: Operation[] = [] - - if (orientation === Orientation.RIGHT_TOP) { - operations.push({ type: 'rotate', numRotations: 1 }) - } else if (orientation === Orientation.BOTTOM_RIGHT) { - operations.push({ type: 'rotate', numRotations: 2 }) - } else if (orientation === Orientation.LEFT_BOTTOM) { - operations.push({ type: 'rotate', numRotations: 3 }) - } else { - // TODO: support more orientations - // eslint-disable-next-line @typescript-eslint/no-unused-vars - // const _: never = orientation - } - - operations.push({ type: 'resize', width }) - - if (contentType === AVIF) { - optimizedBuffer = await processBuffer( - upstreamBuffer, - operations, - 'avif', - quality - ) - } else if (contentType === WEBP) { - optimizedBuffer = await processBuffer( - upstreamBuffer, - operations, - 'webp', - quality - ) - } else if (contentType === PNG) { - optimizedBuffer = await processBuffer( - upstreamBuffer, - operations, - 'png', - quality - ) - } else if (contentType === JPEG) { - optimizedBuffer = await processBuffer( - upstreamBuffer, - operations, - 'jpeg', - quality - ) - } - - // End Squoosh transformation logic - } + let optimizedBuffer = await optimizeImage({ + buffer: upstreamBuffer, + contentType, + quality, + width, + nextConfigOutput: nextConfig.output, + }) if (optimizedBuffer) { if (isDev && width <= BLUR_IMG_SIZE && quality === BLUR_QUALITY) { // During `next dev`, we don't want to generate blur placeholders with webpack @@ -743,52 +759,6 @@ export function sendResponse( } } -export async function resizeImage( - content: Buffer, - width: number, - height: number, - // Should match VALID_BLUR_EXT - extension: 'avif' | 'webp' | 'png' | 'jpeg', - quality: number -): Promise { - if (isAnimated(content)) { - return content - } else if (sharp) { - const transformer = sharp(content) - - if (extension === 'avif') { - if (transformer.avif) { - transformer.avif({ quality }) - } else { - console.warn( - chalk.yellow.bold('Warning: ') + - `Your installed version of the 'sharp' package does not support AVIF images. Run 'yarn add sharp@latest' to upgrade to the latest version.\n` + - 'Read more: https://nextjs.org/docs/messages/sharp-version-avif' - ) - transformer.webp({ quality }) - } - } else if (extension === 'webp') { - transformer.webp({ quality }) - } else if (extension === 'png') { - transformer.png({ quality }) - } else if (extension === 'jpeg') { - transformer.jpeg({ quality }) - } - transformer.resize(width, height) - const buf = await transformer.toBuffer() - return buf - } else { - const resizeOperationOpts: Operation = { type: 'resize', width, height } - const buf = await processBuffer( - content, - [resizeOperationOpts], - extension, - quality - ) - return buf - } -} - export async function getImageSize( buffer: Buffer, // Should match VALID_BLUR_EXT From b827df5497566ced93ab61ae8e88b0c4abf08397 Mon Sep 17 00:00:00 2001 From: Steven Date: Sun, 11 Dec 2022 21:26:36 -0500 Subject: [PATCH 2/3] Skip animated --- .../next/build/webpack/loaders/next-image-loader.ts | 10 +++++++--- packages/next/server/image-optimizer.ts | 4 ---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/next/build/webpack/loaders/next-image-loader.ts b/packages/next/build/webpack/loaders/next-image-loader.ts index 1339ebdb052e8e9..a2453ded1553ec8 100644 --- a/packages/next/build/webpack/loaders/next-image-loader.ts +++ b/packages/next/build/webpack/loaders/next-image-loader.ts @@ -1,3 +1,4 @@ +import isAnimated from 'next/dist/compiled/is-animated' import loaderUtils from 'next/dist/compiled/loader-utils3' import { optimizeImage, getImageSize } from '../../../server/image-optimizer' @@ -73,15 +74,18 @@ function nextImageLoader(this: any, content: Buffer) { blurDataURL = url.href.slice(prefix.length) } else { const resizeImageSpan = imageLoaderSpan.traceChild('image-resize') - const resizedImage = await resizeImageSpan.traceAsyncFn(() => - optimizeImage({ + const resizedImage = await resizeImageSpan.traceAsyncFn(() => { + if (isAnimated(content)) { + return content + } + return optimizeImage({ buffer: content, width: blurWidth, height: blurHeight, contentType: 'image/' + extension, quality: BLUR_QUALITY, }) - ) + }) const blurDataURLSpan = imageLoaderSpan.traceChild( 'image-base64-tostring' ) diff --git a/packages/next/server/image-optimizer.ts b/packages/next/server/image-optimizer.ts index 1905fe46f764f40..6dc952e90c4bdc8 100644 --- a/packages/next/server/image-optimizer.ts +++ b/packages/next/server/image-optimizer.ts @@ -405,10 +405,6 @@ export async function optimizeImage({ height?: number nextConfigOutput?: 'standalone' }): Promise { - if (isAnimated(buffer)) { - return buffer - } - let optimizedBuffer = buffer if (sharp) { // Begin sharp transformation logic From bc104e8d8df5b47b5e04b824a9040126ef86ff42 Mon Sep 17 00:00:00 2001 From: Steven Date: Sun, 11 Dec 2022 21:39:47 -0500 Subject: [PATCH 3/3] Use template string --- packages/next/build/webpack/loaders/next-image-loader.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/next/build/webpack/loaders/next-image-loader.ts b/packages/next/build/webpack/loaders/next-image-loader.ts index a2453ded1553ec8..cc33c6c532a231d 100644 --- a/packages/next/build/webpack/loaders/next-image-loader.ts +++ b/packages/next/build/webpack/loaders/next-image-loader.ts @@ -82,7 +82,7 @@ function nextImageLoader(this: any, content: Buffer) { buffer: content, width: blurWidth, height: blurHeight, - contentType: 'image/' + extension, + contentType: `image/${extension}`, quality: BLUR_QUALITY, }) })