Skip to content

Commit

Permalink
feat: add experiment for sharpjs cpu flags (vercel#71733)
Browse files Browse the repository at this point in the history
This PR adds two experimental flags to configure sharpjs settings that
may improve cpu for image optimization.
  • Loading branch information
styfle authored Oct 24, 2024
1 parent dbc9452 commit 0dce6bb
Show file tree
Hide file tree
Showing 4 changed files with 63 additions and 4 deletions.
3 changes: 3 additions & 0 deletions packages/next/src/server/config-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,9 @@ export const configSchema: zod.ZodType<NextConfig> = z.lazy(() =>
forceSwcTransforms: z.boolean().optional(),
fullySpecified: z.boolean().optional(),
gzipSize: z.boolean().optional(),
imgOptConcurrency: z.number().int().optional().nullable(),
imgOptTimeoutInSeconds: z.number().int().optional(),
imgOptMaxInputPixels: z.number().int().optional(),
internal_disableSyncDynamicAPIWarnings: z.boolean().optional(),
isrFlushToDisk: z.boolean().optional(),
largePageDataBytes: z.number().optional(),
Expand Down
6 changes: 6 additions & 0 deletions packages/next/src/server/config-shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,9 @@ export interface ExperimentalConfig {
extensionAlias?: Record<string, any>
allowedRevalidateHeaderKeys?: string[]
fetchCacheKeyPrefix?: string
imgOptConcurrency?: number | null
imgOptTimeoutInSeconds?: number
imgOptMaxInputPixels?: number
optimisticClientCache?: boolean
/**
* @deprecated use config.expireTime instead
Expand Down Expand Up @@ -1112,6 +1115,9 @@ export const defaultConfig: NextConfig = {
(os.cpus() || { length: 1 }).length) - 1
),
memoryBasedWorkersCount: false,
imgOptConcurrency: null,
imgOptTimeoutInSeconds: 7,
imgOptMaxInputPixels: 268_402_689, // https://sharp.pixelplumbing.com/api-constructor#:~:text=%5Boptions.limitInputPixels%5D
isrFlushToDisk: true,
workerThreads: false,
proxyTimeout: undefined,
Expand Down
27 changes: 23 additions & 4 deletions packages/next/src/server/image-optimizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ const BLUR_QUALITY = 70 // should match `next-image-loader`

let _sharp: typeof import('sharp')

function getSharp() {
function getSharp(concurrency: number | null | undefined) {
if (_sharp) {
return _sharp
}
Expand All @@ -59,7 +59,7 @@ function getSharp() {
// https://sharp.pixelplumbing.com/api-utility#concurrency
const divisor = process.env.NODE_ENV === 'development' ? 4 : 2
_sharp.concurrency(
Math.floor(Math.max(_sharp.concurrency() / divisor, 1))
concurrency ?? Math.floor(Math.max(_sharp.concurrency() / divisor, 1))
)
}
} catch (e: unknown) {
Expand Down Expand Up @@ -512,15 +512,27 @@ export async function optimizeImage({
quality,
width,
height,
concurrency,
limitInputPixels,
timeoutInSeconds,
}: {
buffer: Buffer
contentType: string
quality: number
width: number
height?: number
concurrency?: number | null
limitInputPixels?: number
timeoutInSeconds?: number
}): Promise<Buffer> {
const sharp = getSharp()
const transformer = sharp(buffer).timeout({ seconds: 7 }).rotate()
const sharp = getSharp(concurrency)
const transformer = sharp(buffer, {
limitInputPixels,
})
.timeout({
seconds: timeoutInSeconds ?? 7,
})
.rotate()

if (height) {
transformer.resize(width, height)
Expand Down Expand Up @@ -631,6 +643,10 @@ export async function imageOptimizer(
'href' | 'width' | 'quality' | 'mimeType'
>,
nextConfig: {
experimental: Pick<
NextConfigComplete['experimental'],
'imgOptConcurrency' | 'imgOptMaxInputPixels' | 'imgOptTimeoutInSeconds'
>
images: Pick<
NextConfigComplete['images'],
'dangerouslyAllowSVG' | 'minimumCacheTTL'
Expand Down Expand Up @@ -735,6 +751,9 @@ export async function imageOptimizer(
contentType,
quality,
width,
concurrency: nextConfig.experimental.imgOptConcurrency,
limitInputPixels: nextConfig.experimental.imgOptMaxInputPixels,
timeoutInSeconds: nextConfig.experimental.imgOptTimeoutInSeconds,
})
if (optimizedBuffer) {
if (isDev && width <= BLUR_IMG_SIZE && quality === BLUR_QUALITY) {
Expand Down
31 changes: 31 additions & 0 deletions test/integration/image-optimizer/test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -734,6 +734,37 @@ describe('Image Optimizer', () => {
})
})

describe('experimental.imgOptMaxInputPixels in next.config.js', () => {
let app
let appPort

beforeAll(async () => {
nextConfig.replace(
'{ /* replaceme */ }',
JSON.stringify({
experimental: {
imgOptMaxInputPixels: 100,
},
})
)
await cleanImagesDir({ imagesDir })
appPort = await findPort()
app = await launchApp(appDir, appPort)
})
afterAll(async () => {
await killApp(app)
nextConfig.restore()
})
it('should fallback to source image when input exceeds imgOptMaxInputPixels', async () => {
const size = 256 // defaults defined in lib/image-config.ts
const query = { w: size, q: 75, url: '/test.jpg' }
const opts = { headers: { accept: 'image/webp' } }
const res = await fetchViaHTTP(appPort, '/_next/image', query, opts)
expect(res.status).toBe(200)
expect(res.headers.get('Content-Type')).toBe('image/jpeg')
})
})

describe('External rewrite support with for serving static content in images', () => {
;(process.env.TURBOPACK_DEV ? describe.skip : describe)(
'production mode',
Expand Down

0 comments on commit 0dce6bb

Please sign in to comment.