perf: do not use next/dist/server image optimizer (#23207)

* write image utils

* refactor

* better comment
This commit is contained in:
Benny Joo
2025-08-25 15:13:44 +05:30
committed by GitHub
parent 5bab2af500
commit 8e73421094
2 changed files with 124 additions and 8 deletions
+10 -8
View File
@@ -193,25 +193,27 @@ async function getHandler(request: NextRequest) {
const response = await fetch(filteredLogo);
const arrayBuffer = await response.arrayBuffer();
let buffer: Buffer = Buffer.from(arrayBuffer);
let contentType = response.headers.get("content-type") || "image/png";
// If we need to resize the team logos (via Next.js' built-in image processing)
// Resize the team logos if needed
if (teamLogos[logoDefinition.source] && logoDefinition.w) {
const { detectContentType, optimizeImage } = await import("next/dist/server/image-optimizer");
buffer = await optimizeImage({
const { resizeImage } = await import("@calcom/lib/server/imageUtils");
const { buffer: outBuffer, contentType: outContentType } = await resizeImage({
buffer,
contentType: (await detectContentType(buffer)) ?? "image/jpeg",
quality: 100,
width: logoDefinition.w,
height: logoDefinition.h, // optional
height: logoDefinition.h,
quality: 100,
contentType,
});
buffer = outBuffer;
contentType = outContentType;
}
// Create a new response with the image buffer
const imageResponse = new NextResponse(buffer as BodyInit);
// Set the appropriate headers
imageResponse.headers.set("Content-Type", response.headers.get("content-type") || "image/png");
imageResponse.headers.set("Content-Type", contentType);
imageResponse.headers.set("Cache-Control", "s-maxage=86400, stale-while-revalidate=60");
return imageResponse;
+114
View File
@@ -3,6 +3,13 @@ import sharp from "sharp";
// Maximum allowed size for SVG data (5MB)
const MAX_SVG_SIZE = 5 * 1024 * 1024;
const JPEG = "image/jpeg";
const PNG = "image/png";
const WEBP = "image/webp";
const AVIF = "image/avif";
const GIF = "image/gif";
const SVG = "image/svg+xml";
/**
* Converts an SVG image to PNG format
* @param data Base64 encoded image data
@@ -29,3 +36,110 @@ export const convertSvgToPng = async (data: string) => {
}
return data;
};
/**
* Detect content type from image buffer.
* Simplified version of Next.js image optimizer's detectContentType function (https://github.com/vercel/next.js/blob/9436dce61f1a3ff9478261dc2eba47e0527acf3d/packages/next/src/server/image-optimizer.ts#L160).
* Supports common web image formats (JPEG, PNG, GIF, WEBP, AVIF, SVG) and drops
* irrelevant formats like PDF, ICO, TIFF, etc. that aren't used for logos.
*/
export async function detectContentType(buffer: Buffer): Promise<string | null> {
if ([0xff, 0xd8, 0xff].every((b, i) => buffer[i] === b)) {
return JPEG;
}
if ([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a].every((b, i) => buffer[i] === b)) {
return PNG;
}
if ([0x47, 0x49, 0x46, 0x38].every((b, i) => buffer[i] === b)) {
return GIF;
}
if ([0x52, 0x49, 0x46, 0x46, 0, 0, 0, 0, 0x57, 0x45, 0x42, 0x50].every((b, i) => !b || buffer[i] === b)) {
return WEBP;
}
if ([0x3c, 0x3f, 0x78, 0x6d, 0x6c].every((b, i) => buffer[i] === b)) {
return SVG;
}
if ([0x3c, 0x73, 0x76, 0x67].every((b, i) => buffer[i] === b)) {
return SVG;
}
if ([0, 0, 0, 0, 0x66, 0x74, 0x79, 0x70, 0x61, 0x76, 0x69, 0x66].every((b, i) => !b || buffer[i] === b)) {
return AVIF;
}
// Fallback to sharp metadata detection
try {
const meta = await sharp(buffer).metadata();
switch (meta?.format) {
case "avif":
return AVIF;
case "webp":
return WEBP;
case "png":
return PNG;
case "jpeg":
case "jpg":
return JPEG;
case "gif":
return GIF;
case "svg":
return SVG;
default:
return null;
}
} catch {
return null;
}
}
/**
* Resize an image buffer while preserving the original format.
* Simplified version of Next.js image optimizer's optimizeImage function (https://github.com/vercel/next.js/blob/9436dce61f1a3ff9478261dc2eba47e0527acf3d/packages/next/src/server/image-optimizer.ts#L640).
* Supports common web image formats (JPEG, PNG, WEBP, AVIF) with format-specific
* optimization settings. Drops advanced options like limitInputPixels, sequentialRead,
* and timeout that aren't needed for logo processing. Uses failOnError: false for
* better robustness with potentially malformed images.
*/
export async function resizeImage(params: {
buffer: Buffer;
width: number;
height?: number;
quality?: number;
contentType?: string;
}): Promise<{ buffer: Buffer; contentType: string }> {
const { buffer, width, height, quality = 100 } = params;
let { contentType } = params;
// Auto-detect content type if not provided
if (!contentType) {
contentType = (await detectContentType(buffer)) ?? PNG;
}
const transformer = sharp(buffer).rotate();
if (height) {
transformer.resize(width, height);
} else {
transformer.resize(width, undefined, { withoutEnlargement: true });
}
// Apply format-specific optimization (preserving original format)
if (contentType === AVIF) {
transformer.avif({
quality: Math.max(quality - 20, 1),
effort: 3,
});
} else if (contentType === WEBP) {
transformer.webp({ quality });
} else if (contentType === PNG) {
transformer.png({ quality });
} else if (contentType === JPEG) {
transformer.jpeg({ quality, mozjpeg: true });
} else {
// For unknown formats, default to PNG
transformer.png({ quality });
contentType = PNG;
}
const optimizedBuffer = await transformer.toBuffer();
return { buffer: optimizedBuffer, contentType };
}