Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use Sharp if available for Image Optimization #27346

Merged
merged 21 commits into from
Jul 22, 2021
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/basic-features/image-optimization.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ module.exports = {

The following Image Optimization cloud providers are included:

- [Vercel](https://vercel.com): Works automatically when you deploy on Vercel, no configuration necessary. [Learn more](https://vercel.com/docs/next.js/image-optimization)
- [Vercel](https://vercel.com): Works automatically when you deploy on Vercel, no configuration necessary. [Learn more](/docs/next.js/image-optimization)
ijjk marked this conversation as resolved.
Show resolved Hide resolved
- [Imgix](https://www.imgix.com): `loader: 'imgix'`
- [Cloudinary](https://cloudinary.com): `loader: 'cloudinary'`
- [Akamai](https://www.akamai.com): `loader: 'akamai'`
Expand All @@ -128,6 +128,8 @@ If you need a different provider, you can use the [`loader`](/docs/api-reference

> The `next/image` component's default loader is not supported when using [`next export`](/docs/advanced-features/static-html-export.md). However, other loader options will work.

> By default, Next.js uses the ['squoosh'](https://github.com/GoogleChromeLabs/squoosh/tree/dev/cli) library for image resizing and optimization. This library is quick to install and suitable for a dev server environment. If you are going to use the default image loader and built-in image optimizer in production, it is strongly recommended that you install the optional [`sharp`](https://github.com/lovell/sharp) image optimization library. Do this by running `yarn add sharp` in your application directory.
atcastle marked this conversation as resolved.
Show resolved Hide resolved

## Caching

The following describes the caching algorithm for the default [loader](#loader). For all other loaders, please refer to your cloud provider's documentation.
Expand Down
16 changes: 10 additions & 6 deletions packages/next/build/webpack/loaders/next-image-loader.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import loaderUtils from 'next/dist/compiled/loader-utils'
import sizeOf from 'image-size'
import { processBuffer } from '../../../server/lib/squoosh/main'
import { resizeImage } from '../../../server/image-optimizer'

const BLUR_IMG_SIZE = 8
const BLUR_QUALITY = 70
Expand Down Expand Up @@ -38,14 +38,18 @@ function nextImageLoader(content) {
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 dimension =
imageSize.width >= imageSize.height ? 'width' : 'height'

const resizeImageSpan = imageLoaderSpan.traceChild('image-resize')
const resizedImage = await resizeImageSpan.traceAsyncFn(() =>
processBuffer(content, [resizeOperationOpts], extension, BLUR_QUALITY)
resizeImage(
content,
dimension,
BLUR_IMG_SIZE,
extension,
BLUR_QUALITY
)
)
const blurDataURLSpan = imageLoaderSpan.traceChild(
'image-base64-tostring'
Expand Down
159 changes: 120 additions & 39 deletions packages/next/server/image-optimizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,15 @@ const VECTOR_TYPES = [SVG]
const BLUR_IMG_SIZE = 8 // should match `next-image-loader`
const inflightRequests = new Map<string, Promise<undefined>>()

let sharp: any
styfle marked this conversation as resolved.
Show resolved Hide resolved
try {
sharp = require('sharp')
} catch (e) {
// Sharp not present on the server, Squoosh fallback will be used
}

let shouldShowSharpWarning = process.env.NODE_ENV === 'production'

export async function imageOptimizer(
server: Server,
req: IncomingMessage,
Expand Down Expand Up @@ -322,52 +331,85 @@ export async function imageOptimizer(
} else {
contentType = JPEG
}

try {
const orientation = await getOrientation(upstreamBuffer)
let optimizedBuffer: Buffer | undefined
if (sharp) {
// Begin sharp transformation logic
const transformer = sharp(upstreamBuffer)

const operations: Operation[] = []
transformer.rotate()

const { width: metaWidth } = await transformer.metadata()

if (metaWidth && metaWidth > width) {
transformer.resize(width)
}

if (contentType === WEBP) {
transformer.webp({ quality })
} else if (contentType === PNG) {
transformer.png({ quality })
} else if (contentType === JPEG) {
transformer.jpeg({ quality })
}

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 })
optimizedBuffer = await transformer.toBuffer()
// End sharp transformation logic
} else {
// TODO: support more orientations
// eslint-disable-next-line @typescript-eslint/no-unused-vars
// const _: never = orientation
}
// Show sharp warning in production once
if (shouldShowSharpWarning) {
console.warn(
`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.`
)
shouldShowSharpWarning = false
}

operations.push({ type: 'resize', width })
// Begin Squoosh transformation logic
const orientation = await getOrientation(upstreamBuffer)

let optimizedBuffer: Buffer | undefined
//if (contentType === AVIF) {
//} 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
)
}
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) {
//} 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
}
if (optimizedBuffer) {
await writeToCacheDir(
hashDir,
Expand Down Expand Up @@ -551,3 +593,42 @@ export function getMaxAge(str: string | null): number {
}
return 0
}

export async function resizeImage(
content: Buffer,
dimension: 'width' | 'height',
size: number,
extension: 'webp' | 'png' | 'jpeg',
quality: number
): Promise<Buffer> {
if (sharp) {
const transformer = sharp(content)

if (extension === 'webp') {
transformer.webp({ quality })
} else if (extension === 'png') {
transformer.png({ quality })
} else if (extension === 'jpeg') {
transformer.jpeg({ quality })
}
if (dimension === 'width') {
transformer.resize(size)
} else {
transformer.resize(null, size)
}
const buf = await transformer.toBuffer()
return buf
} else {
const resizeOperationOpts: Operation =
dimension === 'width'
? { type: 'resize', width: size }
: { type: 'resize', height: size }
const buf = await processBuffer(
content,
[resizeOperationOpts],
extension,
quality
)
return buf
}
}