fix(next/image): use image-size pkg as additional format detector (#82538)

In a previous PR https://github.com/vercel/next.js/pull/82118 we added a
fallback to `sharp().metadata()` when the magic number didn't detect the
image format in order to improve detection.

But `sharp` can be slow to read metadata, so we should avoid it if we
can.

So this PR uses the existing `image-size` package to detect the content
type in JS and only then does it fallback to the native `sharp` code if
necessary.

No tests were added since this doesn't change the behavior, only
performance.
This commit is contained in:
Steven
2025-08-11 19:54:37 +00:00
committed by GitHub
parent 2c1a3d153f
commit ee8286f126
4 changed files with 70 additions and 5 deletions
File diff suppressed because one or more lines are too long
+30 -5
View File
@@ -4,6 +4,7 @@ import type { IncomingMessage, ServerResponse } from 'http'
import { mediaType } from 'next/dist/compiled/@hapi/accept'
import contentDisposition from 'next/dist/compiled/content-disposition'
import imageSizeOf from 'next/dist/compiled/image-size'
import { detector } from 'next/dist/compiled/image-detector/detector.js'
import isAnimated from 'next/dist/compiled/is-animated'
import { join } from 'path'
import nodeUrl, { type UrlWithParsedQuery } from 'url'
@@ -232,11 +233,21 @@ export async function detectContentType(
return JP2
}
const sharp = getSharp(null)
const meta = await sharp(buffer)
.metadata()
.catch((_) => null)
switch (meta?.format) {
let format:
| import('sharp').Metadata['format']
| ReturnType<typeof detector>
| undefined
format = detector(buffer)
if (!format) {
const sharp = getSharp(null)
const meta = await sharp(buffer)
.metadata()
.catch((_) => null)
format = meta?.format
}
switch (format) {
case 'avif':
return AVIF
case 'webp':
@@ -251,6 +262,7 @@ export async function detectContentType(
case 'svg':
return SVG
case 'jxl':
case 'jxl-stream':
return JXL
case 'jp2':
return JP2
@@ -259,6 +271,12 @@ export async function detectContentType(
return TIFF
case 'pdf':
return PDF
case 'bmp':
return BMP
case 'ico':
return ICO
case 'icns':
return ICNS
case 'dcraw':
case 'dz':
case 'exr':
@@ -271,6 +289,13 @@ export async function detectContentType(
case 'rad':
case 'raw':
case 'v':
case 'cur':
case 'dds':
case 'j2c':
case 'ktx':
case 'pnm':
case 'psd':
case 'tga':
case undefined:
default:
return null
+12
View File
@@ -660,6 +660,17 @@ export async function ncc_image_size(task, opts) {
.target('src/compiled/image-size')
}
// eslint-disable-next-line camelcase
externals['image-detector'] = 'next/dist/compiled/image-detector'
export async function ncc_image_detector(task, opts) {
// NOTE: remove this special compile step if the upstream PR lands
// https://github.com/image-size/image-size/pull/451
await task
.source(relative(__dirname, require.resolve('image-size/dist/detector.js')))
.ncc({ packageName: 'image-size', externals })
.target('src/compiled/image-detector')
}
// eslint-disable-next-line camelcase
externals['@hapi/accept'] = 'next/dist/compiled/@hapi/accept'
export async function ncc_hapi_accept(task, opts) {
@@ -2281,6 +2292,7 @@ export async function ncc(task, opts) {
'ncc_p_queue',
'ncc_raw_body',
'ncc_image_size',
'ncc_image_detector',
'ncc_hapi_accept',
'ncc_commander',
'ncc_node_anser',
+27
View File
@@ -464,6 +464,33 @@ declare module 'next/dist/compiled/image-size' {
export = m
}
declare module 'next/dist/compiled/image-detector/detector.js' {
export function detector(
arr: Uint8Array
):
| 'bmp'
| 'cur'
| 'dds'
| 'gif'
| 'heif'
| 'icns'
| 'ico'
| 'j2c'
| 'jp2'
| 'jpg'
| 'jxl'
| 'jxl-stream'
| 'ktx'
| 'png'
| 'pnm'
| 'psd'
| 'svg'
| 'tga'
| 'tiff'
| 'webp'
| undefined
}
declare module 'next/dist/compiled/@hapi/accept' {
import m from '@hapi/accept'
export = m