From 241a916e03df4f032ec715ad518d01292652815c Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Thu, 18 Mar 2021 23:51:36 +0800 Subject: [PATCH] Improve image optimizer to only create 1 worker thread (#23188) As stated in #23157, this PR merges all the operations into 1 worker thread (`processBuffer` in `impl.ts`) and only pass a list of operation names and args into the worker. This should improve the speed and memory usage of next/image. Fixes #23157. X-ref: #22925. --- .../next-server/server/image-optimizer.ts | 45 ++++++++------- .../next-server/server/lib/squoosh/impl.ts | 56 +++++++++++++++---- .../next-server/server/lib/squoosh/main.ts | 51 ++++------------- 3 files changed, 82 insertions(+), 70 deletions(-) diff --git a/packages/next/next-server/server/image-optimizer.ts b/packages/next/next-server/server/image-optimizer.ts index 1b76719dfe..69bb10f261 100644 --- a/packages/next/next-server/server/image-optimizer.ts +++ b/packages/next/next-server/server/image-optimizer.ts @@ -10,15 +10,7 @@ import Stream from 'stream' import nodeUrl, { UrlWithParsedQuery } from 'url' import { fileExists } from '../../lib/file-exists' import { ImageConfig, imageConfigDefault } from './image-config' -import ImageData from './lib/squoosh/image_data' -import { - decodeBuffer, - encodeJpeg, - encodePng, - encodeWebp, - resize, - rotate, -} from './lib/squoosh/main' +import { processBuffer, Operation } from './lib/squoosh/main' import Server from './next-server' import { sendEtagResponse } from './send-payload' import { getContentType, getExtension } from './serve-static' @@ -251,33 +243,48 @@ export async function imageOptimizer( } try { - let bitmap: ImageData = await decodeBuffer(upstreamBuffer) const orientation = await getOrientation(upstreamBuffer) + + const operations: Operation[] = [] + if (orientation === Orientation.RIGHT_TOP) { - bitmap = await rotate(bitmap, 1) + operations.push({ type: 'rotate', numRotations: 1 }) } else if (orientation === Orientation.BOTTOM_RIGHT) { - bitmap = await rotate(bitmap, 2) + operations.push({ type: 'rotate', numRotations: 2 }) } else if (orientation === Orientation.LEFT_BOTTOM) { - bitmap = await rotate(bitmap, 3) + operations.push({ type: 'rotate', numRotations: 3 }) } else { // TODO: support more orientations // eslint-disable-next-line @typescript-eslint/no-unused-vars // const _: never = orientation } - if (bitmap.width && bitmap.width > width) { - bitmap = await resize(bitmap, { width }) - } + operations.push({ type: 'resize', width }) let optimizedBuffer: Buffer | undefined //if (contentType === AVIF) { //} else if (contentType === WEBP) { - optimizedBuffer = await encodeWebp(bitmap, { quality }) + optimizedBuffer = await processBuffer( + upstreamBuffer, + operations, + 'webp', + quality + ) } else if (contentType === PNG) { - optimizedBuffer = await encodePng(bitmap) + optimizedBuffer = await processBuffer( + upstreamBuffer, + operations, + 'png', + quality + ) } else if (contentType === JPEG) { - optimizedBuffer = await encodeJpeg(bitmap, { quality }) + optimizedBuffer = await processBuffer( + upstreamBuffer, + operations, + 'jpeg', + quality + ) } if (optimizedBuffer) { diff --git a/packages/next/next-server/server/lib/squoosh/impl.ts b/packages/next/next-server/server/lib/squoosh/impl.ts index c18ee801c9..2eee7996b3 100644 --- a/packages/next/next-server/server/lib/squoosh/impl.ts +++ b/packages/next/next-server/server/lib/squoosh/impl.ts @@ -1,9 +1,47 @@ import { codecs as supportedFormats, preprocessors } from './codecs' import ImageData from './image_data' -export async function decodeBuffer( - _buffer: Buffer | Uint8Array -): Promise { +type RotateOperation = { + type: 'rotate' + numRotations: number +} +type ResizeOperation = { + type: 'resize' + width: number +} +export type Operation = RotateOperation | ResizeOperation +export type Encoding = 'jpeg' | 'png' | 'webp' + +export async function processBuffer( + buffer: Buffer | Uint8Array, + operations: Operation[], + encoding: Encoding, + quality: number +): Promise { + let imageData = await decodeBuffer(buffer) + for (const operation of operations) { + if (operation.type === 'rotate') { + imageData = await rotate(imageData, operation.numRotations) + } else if (operation.type === 'resize') { + if (imageData.width && imageData.width > operation.width) { + imageData = await resize(imageData, operation.width) + } + } + } + + switch (encoding) { + case 'jpeg': + return encodeJpeg(imageData, { quality }) + case 'webp': + return encodeWebp(imageData, { quality }) + case 'png': + return encodePng(imageData) + default: + throw Error(`Unsupported encoding format`) + } +} + +async function decodeBuffer(_buffer: Buffer | Uint8Array): Promise { const buffer = Buffer.from(_buffer) const firstChunk = buffer.slice(0, 16) const firstChunkString = Array.from(firstChunk) @@ -20,7 +58,7 @@ export async function decodeBuffer( return rgba } -export async function rotate( +async function rotate( image: ImageData, numRotations: number ): Promise { @@ -30,7 +68,7 @@ export async function rotate( return await m(image.data, image.width, image.height, { numRotations }) } -export async function resize(image: ImageData, { width }: { width: number }) { +async function resize(image: ImageData, width: number) { image = ImageData.from(image) const p = preprocessors['resize'] @@ -41,7 +79,7 @@ export async function resize(image: ImageData, { width }: { width: number }) { }) } -export async function encodeJpeg( +async function encodeJpeg( image: ImageData, { quality }: { quality: number } ): Promise { @@ -56,7 +94,7 @@ export async function encodeJpeg( return Buffer.from(r) } -export async function encodeWebp( +async function encodeWebp( image: ImageData, { quality }: { quality: number } ): Promise { @@ -71,9 +109,7 @@ export async function encodeWebp( return Buffer.from(r) } -export async function encodePng( - image: ImageData -): Promise { +async function encodePng(image: ImageData): Promise { image = ImageData.from(image) const e = supportedFormats['oxipng'] diff --git a/packages/next/next-server/server/lib/squoosh/main.ts b/packages/next/next-server/server/lib/squoosh/main.ts index 4a778aa7ce..3334664bcd 100644 --- a/packages/next/next-server/server/lib/squoosh/main.ts +++ b/packages/next/next-server/server/lib/squoosh/main.ts @@ -1,7 +1,7 @@ import { Worker } from 'jest-worker' import * as path from 'path' import { execOnce } from '../../../lib/utils' -import ImageData from './image_data' +import { Operation, Encoding } from './impl' const getWorker = execOnce( () => @@ -10,47 +10,16 @@ const getWorker = execOnce( }) ) -export async function decodeBuffer(buffer: Buffer): Promise { - const worker: typeof import('./impl') = getWorker() as any - return ImageData.from(await worker.decodeBuffer(buffer)) -} - -export async function rotate( - image: ImageData, - numRotations: number -): Promise { - const worker: typeof import('./impl') = getWorker() as any - return ImageData.from(await worker.rotate(image, numRotations)) -} - -export async function resize( - image: ImageData, - { width }: { width: number } -): Promise { - const worker: typeof import('./impl') = getWorker() as any - return ImageData.from(await worker.resize(image, { width })) -} +export { Operation } -export async function encodeJpeg( - image: ImageData, - { quality }: { quality: number } +export async function processBuffer( + buffer: Buffer, + operations: Operation[], + encoding: Encoding, + quality: number ): Promise { const worker: typeof import('./impl') = getWorker() as any - const o = await worker.encodeJpeg(image, { quality }) - return Buffer.from(o) -} - -export async function encodeWebp( - image: ImageData, - { quality }: { quality: number } -): Promise { - const worker: typeof import('./impl') = getWorker() as any - const o = await worker.encodeWebp(image, { quality }) - return Buffer.from(o) -} - -export async function encodePng(image: ImageData): Promise { - const worker: typeof import('./impl') = getWorker() as any - const o = await worker.encodePng(image) - return Buffer.from(o) + return Buffer.from( + await worker.processBuffer(buffer, operations, encoding, quality) + ) } -- GitLab