diff --git a/.changeset/rich-dolphins-teach.md b/.changeset/rich-dolphins-teach.md new file mode 100644 index 000000000000..a9bbf7de9bb1 --- /dev/null +++ b/.changeset/rich-dolphins-teach.md @@ -0,0 +1,5 @@ +--- +'@astrojs/image': patch +--- + +Parallelize image transforms diff --git a/packages/integrations/image/package.json b/packages/integrations/image/package.json index 045569767cb6..fb1aaa67be18 100644 --- a/packages/integrations/image/package.json +++ b/packages/integrations/image/package.json @@ -40,6 +40,7 @@ "test": "mocha --exit --timeout 20000 test" }, "dependencies": { + "@altano/tiny-async-pool": "^1.0.2", "image-size": "^1.0.2", "magic-string": "^0.25.9", "mime": "^3.0.0", diff --git a/packages/integrations/image/src/build/ssg.ts b/packages/integrations/image/src/build/ssg.ts index 4602ef9356e1..6ee167ce6695 100644 --- a/packages/integrations/image/src/build/ssg.ts +++ b/packages/integrations/image/src/build/ssg.ts @@ -7,6 +7,8 @@ import type { SSRImageService, TransformOptions } from '../loaders/index.js'; import { loadLocalImage, loadRemoteImage } from '../utils/images.js'; import { debug, info, LoggerLevel, warn } from '../utils/logger.js'; import { isRemoteImage } from '../utils/paths.js'; +import OS from 'node:os'; +import { doWork } from '@altano/tiny-async-pool'; function getTimeStat(timeStart: number, timeEnd: number) { const buildTime = timeEnd - timeStart; @@ -23,19 +25,26 @@ export interface SSGBuildParams { export async function ssgBuild({ loader, staticImages, config, outDir, logLevel }: SSGBuildParams) { const timer = performance.now(); + const cpuCount = OS.cpus().length; info({ level: logLevel, prefix: false, message: `${bgGreen( - black(` optimizing ${staticImages.size} image${staticImages.size > 1 ? 's' : ''} `) + black( + ` optimizing ${staticImages.size} image${ + staticImages.size > 1 ? 's' : '' + } in batches of ${cpuCount} ` + ) )}`, }); const inputFiles = new Set(); - // process transforms one original image file at a time - for (let [src, transformsMap] of staticImages) { + async function processStaticImage([src, transformsMap]: [ + string, + Map + ]): Promise { let inputFile: string | undefined = undefined; let inputBuffer: Buffer | undefined = undefined; @@ -60,15 +69,15 @@ export async function ssgBuild({ loader, staticImages, config, outDir, logLevel if (!inputBuffer) { // eslint-disable-next-line no-console warn({ level: logLevel, message: `"${src}" image could not be fetched` }); - continue; + return; } const transforms = Array.from(transformsMap.entries()); - debug({ level: logLevel, prefix: false, message: `${green('▶')} ${src}` }); + debug({ level: logLevel, prefix: false, message: `${green('▶')} transforming ${src}` }); let timeStart = performance.now(); - // process each transformed versiono of the + // process each transformed version for (const [filename, transform] of transforms) { timeStart = performance.now(); let outputFile: string; @@ -92,11 +101,14 @@ export async function ssgBuild({ loader, staticImages, config, outDir, logLevel debug({ level: logLevel, prefix: false, - message: ` ${cyan('└─')} ${dim(pathRelative)} ${dim(timeIncrease)}`, + message: ` ${cyan('created')} ${dim(pathRelative)} ${dim(timeIncrease)}`, }); } } + // transform each original image file in batches + await doWork(cpuCount, staticImages, processStaticImage); + info({ level: logLevel, prefix: false, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1ac4cf7679fd..d1c78c5d5018 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2214,6 +2214,7 @@ importers: packages/integrations/image: specifiers: + '@altano/tiny-async-pool': ^1.0.2 '@types/sharp': ^0.30.5 astro: workspace:* astro-scripts: workspace:* @@ -2223,6 +2224,7 @@ importers: mime: ^3.0.0 sharp: ^0.30.6 dependencies: + '@altano/tiny-async-pool': 1.0.2 image-size: 1.0.2 magic-string: 0.25.9 mime: 3.0.0 @@ -3155,6 +3157,10 @@ packages: '@algolia/requester-common': 4.14.2 dev: false + /@altano/tiny-async-pool/1.0.2: + resolution: {integrity: sha512-qQzaI0TBUPdpjZ3qo5b2ziQY9MSNpbziH2ZrE5lvtUZL+kn9GwVuVJwoOubaoNkeDB+rqEefnpu1k+oMpOCYiw==} + dev: false + /@ampproject/remapping/2.2.0: resolution: {integrity: sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==} engines: {node: '>=6.0.0'}