mirror of
https://github.com/vercel/next.js.git
synced 2026-09-20 02:25:18 +08:00
deed868499
In a previous PR, `mozjpeg` was set to true which might be the reason output jpeg spikes cpu. - https://github.com/vercel/next.js/pull/65846 I made a benchmark and found that disabling MozJPEG saved 72% CPU, with average outputs 31% larger. - https://github.com/lovell/sharp/issues/4603 So this PR adds a new experimental flag to enable (or rather disable) mozjpeg and try with real workloads. This is useful becaues in many cases, cpu is more costly than bandwidth since most CDNs provide [near unlimited bandwidth](https://vercel.com/blog/introducing-flat-rate-cdn).
211 lines
6.4 KiB
TypeScript
211 lines
6.4 KiB
TypeScript
/* eslint-env jest */
|
|
import { readFile } from 'fs-extra'
|
|
import { join } from 'path'
|
|
import {
|
|
getSharp,
|
|
imageOptimizerTransform,
|
|
type ImageOptimizerTransformConfig,
|
|
} from 'next/dist/server/image-optimizer/transform'
|
|
import type {
|
|
CachedRouteKind,
|
|
IncrementalResponseCacheEntry,
|
|
} from 'next/dist/server/response-cache/types'
|
|
|
|
const getImage = (filename: string) =>
|
|
readFile(join(__dirname, 'images', filename))
|
|
|
|
const config = {
|
|
images: {
|
|
dangerouslyAllowSVG: true,
|
|
minimumCacheTTL: 60,
|
|
},
|
|
experimental: {
|
|
imgOptConcurrency: 1,
|
|
imgOptOperationCache: false,
|
|
imgOptMaxInputPixels: 67_108_864,
|
|
imgOptSequentialRead: true,
|
|
imgOptTimeoutInSeconds: 6,
|
|
},
|
|
}
|
|
|
|
async function transform(
|
|
filename: string,
|
|
mimeType = 'image/webp',
|
|
nextConfig: ImageOptimizerTransformConfig = config
|
|
) {
|
|
const buffer = await getImage(filename)
|
|
return imageOptimizerTransform(
|
|
{
|
|
buffer,
|
|
contentType: undefined,
|
|
cacheControl: 'public, max-age=120',
|
|
etag: 'source-etag',
|
|
},
|
|
{ href: `/${filename}`, width: 64, quality: 75, mimeType },
|
|
nextConfig
|
|
)
|
|
}
|
|
|
|
async function transformInDevelopment(
|
|
filename: string,
|
|
previousCacheEntry?: IncrementalResponseCacheEntry
|
|
) {
|
|
const { imageOptimizer } =
|
|
require('next/dist/server/image-optimizer') as typeof import('next/dist/server/image-optimizer')
|
|
const buffer = await getImage(filename)
|
|
return imageOptimizer(
|
|
{
|
|
buffer,
|
|
contentType: undefined,
|
|
cacheControl: undefined,
|
|
etag: 'source-etag',
|
|
},
|
|
{ href: `/${filename}`, width: 8, quality: 70, mimeType: 'image/webp' },
|
|
config,
|
|
{ isDev: true, silent: true, previousCacheEntry }
|
|
)
|
|
}
|
|
|
|
describe('imageOptimizerTransform', () => {
|
|
it('loads without the server image optimizer dependency graph', () => {
|
|
const loaded = Object.keys(require.cache)
|
|
expect(
|
|
loaded.some((path) => /\/server\/image-optimizer\.[jt]s$/.test(path))
|
|
).toBe(false)
|
|
expect(
|
|
loaded.some((path) => path.includes('/server/response-cache/'))
|
|
).toBe(false)
|
|
expect(
|
|
loaded.some((path) => /\/server\/serve-static\.[jt]s$/.test(path))
|
|
).toBe(false)
|
|
expect(
|
|
loaded.some((path) => /\/server\/lib\/mock-request\.[jt]s$/.test(path))
|
|
).toBe(false)
|
|
expect(
|
|
loaded.some((path) => /\/server\/config-shared\.[jt]s$/.test(path))
|
|
).toBe(false)
|
|
expect(
|
|
loaded.some((path) => /\/build\/output\/log\.[jt]s$/.test(path))
|
|
).toBe(false)
|
|
})
|
|
|
|
it('transforms a png buffer', async () => {
|
|
const result = await transform('test.png')
|
|
expect(result.contentType).toBe('image/webp')
|
|
expect(result.buffer.byteLength).toBeGreaterThan(0)
|
|
expect(result.maxAge).toBe(120)
|
|
})
|
|
|
|
it.each([undefined, true, false])(
|
|
'encodes JPEGs with imgOptMozjpeg=%s',
|
|
async (imgOptMozjpeg) => {
|
|
const result = await transform('test.jpg', 'image/jpeg', {
|
|
...config,
|
|
experimental: { ...config.experimental, imgOptMozjpeg },
|
|
})
|
|
expect(result.error).toBeUndefined()
|
|
expect(result.contentType).toBe('image/jpeg')
|
|
const sharp = getSharp(1, false)
|
|
expect(await sharp(result.buffer).metadata()).toMatchObject({
|
|
format: 'jpeg',
|
|
width: 64,
|
|
isProgressive: imgOptMozjpeg ?? true,
|
|
})
|
|
}
|
|
)
|
|
|
|
it('preserves the source format when no output format is requested', async () => {
|
|
const result = await transform('test.png', '')
|
|
expect(result.contentType).toBe('image/png')
|
|
})
|
|
|
|
it('transforms an avif source', async () => {
|
|
const source = await getImage('test.avif')
|
|
const result = await transform('test.avif')
|
|
expect(result.buffer).not.toEqual(source)
|
|
expect(result.contentType).toBe('image/webp')
|
|
})
|
|
|
|
it('downlevels an avif source when no output format is requested', async () => {
|
|
const result = await transform('test.avif', '')
|
|
expect(result.contentType).toBe('image/jpeg')
|
|
})
|
|
|
|
it('generates blur placeholders in development', async () => {
|
|
const result = await transformInDevelopment('test.png')
|
|
expect(result.contentType).toBe('image/svg+xml')
|
|
expect(result.buffer.toString()).toContain('<svg')
|
|
})
|
|
|
|
it('does not generate blur placeholders for bypassed images', async () => {
|
|
const source = await getImage('test.svg')
|
|
const result = await transformInDevelopment('test.svg')
|
|
expect(result.buffer).toEqual(source)
|
|
expect(result.contentType).toBe('image/svg+xml')
|
|
})
|
|
|
|
it('reuses a previous cache entry without generating a blur placeholder', async () => {
|
|
const previousBuffer = Buffer.from('previous image')
|
|
const result = await transformInDevelopment('test.png', {
|
|
value: {
|
|
kind: 'IMAGE' as CachedRouteKind.IMAGE,
|
|
buffer: previousBuffer,
|
|
etag: 'optimized-etag',
|
|
upstreamEtag: 'source-etag',
|
|
extension: 'webp',
|
|
},
|
|
cacheControl: { revalidate: 90, expire: undefined },
|
|
})
|
|
expect(result.buffer).toEqual(previousBuffer)
|
|
expect(result.contentType).toBe('image/webp')
|
|
expect(result.maxAge).toBe(90)
|
|
})
|
|
|
|
it('bypasses svg buffers', async () => {
|
|
const source = await getImage('test.svg')
|
|
const result = await transform('test.svg')
|
|
expect(result.buffer).toEqual(source)
|
|
expect(result.contentType).toBe('image/svg+xml')
|
|
})
|
|
|
|
it('bypasses animated buffers', async () => {
|
|
const source = await getImage('animated.webp')
|
|
const result = await transform('animated.webp')
|
|
expect(result.buffer).toEqual(source)
|
|
expect(result.contentType).toBe('image/webp')
|
|
})
|
|
|
|
it('rejects disallowed svg buffers', async () => {
|
|
await expect(
|
|
imageOptimizerTransform(
|
|
{
|
|
buffer: await getImage('test.svg'),
|
|
contentType: undefined,
|
|
cacheControl: undefined,
|
|
etag: 'source-etag',
|
|
},
|
|
{ href: '/test.svg', width: 64, quality: 75, mimeType: 'image/webp' },
|
|
{
|
|
...config,
|
|
images: { ...config.images, dangerouslyAllowSVG: false },
|
|
}
|
|
)
|
|
).rejects.toMatchObject({ statusCode: 400 })
|
|
})
|
|
|
|
it('rejects unrecognized buffers', async () => {
|
|
await expect(
|
|
imageOptimizerTransform(
|
|
{
|
|
buffer: Buffer.from('not an image'),
|
|
contentType: undefined,
|
|
cacheControl: undefined,
|
|
etag: 'source-etag',
|
|
},
|
|
{ href: '/bad', width: 64, quality: 75, mimeType: 'image/webp' },
|
|
config
|
|
)
|
|
).rejects.toMatchObject({ statusCode: 400 })
|
|
})
|
|
})
|