Improve image optimization run-time and memory usage #457

Merged
a258wang merged 8 commits from amy-image-optimization-quick-fix into main 2022-06-04 19:31:02 -04:00
1 changed files with 65 additions and 36 deletions

View File

@ -16,14 +16,16 @@ import { default as getImageDimensions } from "image-size";
const SOURCE_DIRECTORY = "images"; const SOURCE_DIRECTORY = "images";
const DESTINATION_DIRECTORY = path.join("public", "images"); const DESTINATION_DIRECTORY = path.join("public", "images");
// directory where Meet the Team headshots are stored, relative to the source directory // directories are relative to SOURCE_DIRECTORY
const TEAM_IMAGES_DIRECTORY = path.join("team", ""); const TEAM_IMAGES_DIRECTORY = path.join("team", "");
const EVENTS_IMAGES_DIRECTORY = path.join("events", "");
const IMAGE_MINIMUM_SIZE = 512; const IMAGE_MINIMUM_SIZE = 512;
const GET_ENCODER_FROM_EXTENSION: { [imageExtension: string]: string } = { const GET_ENCODER_FROM_EXTENSION: { [imageExtension: string]: string } = {
jpg: "mozjpeg", jpg: "mozjpeg",
jpeg: "mozjpeg", jpeg: "mozjpeg",
JPG: "mozjpeg",
png: "oxipng", png: "oxipng",
}; };
@ -35,6 +37,8 @@ const ENCODER_OPTIONS: { [encoder: string]: Record<string, unknown> } = {
void optimizeImages(); void optimizeImages();
export async function optimizeImages() { export async function optimizeImages() {
const startTime = Date.now();
Review

By the way, JavaScript has a built in console.time()

https://developer.mozilla.org/en-US/docs/Web/API/console/time

By the way, JavaScript has a built in `console.time()` https://developer.mozilla.org/en-US/docs/Web/API/console/time
const imagePaths = await getFilePathsInDirectory(SOURCE_DIRECTORY); const imagePaths = await getFilePathsInDirectory(SOURCE_DIRECTORY);
await fse.emptyDir(DESTINATION_DIRECTORY); await fse.emptyDir(DESTINATION_DIRECTORY);
@ -42,52 +46,73 @@ export async function optimizeImages() {
const numberOfWorkers = Math.min(cpus().length, 8); const numberOfWorkers = Math.min(cpus().length, 8);
const imagePool = new ImagePool(numberOfWorkers); const imagePool = new ImagePool(numberOfWorkers);
await Promise.all( // process smaller batches in order to reduce memory usage
imagePaths.map(async (imagePath) => { const batchSize = 32;
const sourcePath = path.join(SOURCE_DIRECTORY, imagePath);
const destinationPath = path.join(DESTINATION_DIRECTORY, imagePath);
const fileExtension = imagePath.split(".").pop() ?? "";
const encoder = GET_ENCODER_FROM_EXTENSION[fileExtension];
if (!encoder) { for (let i = 0; i < imagePaths.length; i += batchSize) {
await fse.copy(sourcePath, destinationPath); await Promise.all(
return; imagePaths.slice(i, i + batchSize).map(async (imagePath) => {
} const imageStartTime = Date.now();
const rawImageFile = await fse.readFile(sourcePath); const sourcePath = path.join(SOURCE_DIRECTORY, imagePath);
const ingestedImage = imagePool.ingestImage(rawImageFile); const destinationPath = path.join(DESTINATION_DIRECTORY, imagePath);
const { width, height } = getImageDimensions(rawImageFile); const fileExtension = imagePath.split(".").pop() ?? "";
const encoder = GET_ENCODER_FROM_EXTENSION[fileExtension];
await ingestedImage.decoded; if (!encoder) {
await fse.copy(sourcePath, destinationPath);
console.log(
`Copied ${imagePath} in ${getElapsedSeconds(imageStartTime)}s`
);
return;
}
const shouldResize = const rawImageFile = await fse.readFile(sourcePath);
imagePath.startsWith(TEAM_IMAGES_DIRECTORY) && const ingestedImage = imagePool.ingestImage(rawImageFile);
(width ?? 0) > IMAGE_MINIMUM_SIZE && const { width, height } = getImageDimensions(rawImageFile);
(height ?? 0) > IMAGE_MINIMUM_SIZE;
if (width && height && shouldResize) { await ingestedImage.decoded;
const smallerDimension = width < height ? "width" : "height";
// specifying only one dimension maintains the aspect ratio const shouldResize =
const preprocessOptions = { (imagePath.startsWith(TEAM_IMAGES_DIRECTORY) ||
resize: { imagePath.startsWith(EVENTS_IMAGES_DIRECTORY)) &&
enabled: true, (width ?? 0) > IMAGE_MINIMUM_SIZE &&
[smallerDimension]: IMAGE_MINIMUM_SIZE, (height ?? 0) > IMAGE_MINIMUM_SIZE;
},
};
await ingestedImage.preprocess(preprocessOptions); if (width && height && shouldResize) {
} const smallerDimension = width < height ? "width" : "height";
const encodeOptions = { [encoder]: ENCODER_OPTIONS[encoder] }; // specifying only one dimension maintains the aspect ratio
await ingestedImage.encode(encodeOptions); const preprocessOptions = {
resize: {
enabled: true,
[smallerDimension]: IMAGE_MINIMUM_SIZE,
},
};
const encodedImage = await ingestedImage.encodedWith[encoder]; await ingestedImage.preprocess(preprocessOptions);
await fse.outputFile(destinationPath, encodedImage.binary);
}) console.log(
); `Resized ${sourcePath} in ${getElapsedSeconds(imageStartTime)}s`
);
}
const encodeOptions = { [encoder]: ENCODER_OPTIONS[encoder] };
await ingestedImage.encode(encodeOptions);
const encodedImage = await ingestedImage.encodedWith[encoder];
await fse.outputFile(destinationPath, encodedImage.binary);
console.log(
`Optimized ${sourcePath} in ${getElapsedSeconds(imageStartTime)}s`
);
})
);
}
await imagePool.close(); await imagePool.close();
console.log(`TOTAL DURATION: ${getElapsedSeconds(startTime)}s`);
} }
async function getFilePathsInDirectory(directory: string): Promise<string[]> { async function getFilePathsInDirectory(directory: string): Promise<string[]> {
@ -105,3 +130,7 @@ async function getFilePathsInDirectory(directory: string): Promise<string[]> {
) )
).flat(); ).flat();
} }
function getElapsedSeconds(startTime: number) {
return (Date.now() - startTime) / 1000;
}