Improve image optimization run-time and memory usage #457
|
@ -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();
|
||||||
|
|||||||
|
|
||||||
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;
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue
By the way, JavaScript has a built in
console.time()
https://developer.mozilla.org/en-US/docs/Web/API/console/time