Add caching for optimized images (#6990)
This commit is contained in:
parent
14fd198ea5
commit
818252acda
6 changed files with 127 additions and 13 deletions
5
.changeset/forty-horses-act.md
Normal file
5
.changeset/forty-horses-act.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'astro': minor
|
||||
---
|
||||
|
||||
Generated optimized images are now cached inside the `node_modules/.astro/assets` folder. The cached images will be used to avoid doing extra work and speed up subsequent builds.
|
|
@ -430,6 +430,23 @@ export interface AstroUserConfig {
|
|||
*/
|
||||
outDir?: string;
|
||||
|
||||
/**
|
||||
* @docs
|
||||
* @name cacheDir
|
||||
* @type {string}
|
||||
* @default `"./node_modules/.astro"`
|
||||
* @description Set the directory for caching build artifacts. Files in this directory will be used in subsequent builds to speed up the build time.
|
||||
*
|
||||
* The value can be either an absolute file system path or a path relative to the project root.
|
||||
*
|
||||
* ```js
|
||||
* {
|
||||
* cacheDir: './my-custom-cache-directory'
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
cacheDir?: string;
|
||||
|
||||
/**
|
||||
* @docs
|
||||
* @name site
|
||||
|
|
|
@ -73,13 +73,20 @@ export function getStaticImageList(): Iterable<
|
|||
return globalThis.astroAsset.staticImages?.entries();
|
||||
}
|
||||
|
||||
interface GenerationData {
|
||||
interface GenerationDataUncached {
|
||||
cached: false;
|
||||
weight: {
|
||||
before: number;
|
||||
after: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface GenerationDataCached {
|
||||
cached: true;
|
||||
}
|
||||
|
||||
type GenerationData = GenerationDataUncached | GenerationDataCached;
|
||||
|
||||
export async function generateImage(
|
||||
buildOpts: StaticBuildOptions,
|
||||
options: ImageTransform,
|
||||
|
@ -89,7 +96,19 @@ export async function generateImage(
|
|||
return undefined;
|
||||
}
|
||||
|
||||
const imageService = (await getConfiguredImageService()) as LocalImageService;
|
||||
let useCache = true;
|
||||
const assetsCacheDir = new URL('assets/', buildOpts.settings.config.cacheDir);
|
||||
|
||||
// Ensure that the cache directory exists
|
||||
try {
|
||||
await fs.promises.mkdir(assetsCacheDir, { recursive: true });
|
||||
} catch (err) {
|
||||
console.error(
|
||||
'An error was encountered while creating the cache directory. Proceeding without caching. Error: ',
|
||||
err
|
||||
);
|
||||
useCache = false;
|
||||
}
|
||||
|
||||
let serverRoot: URL, clientRoot: URL;
|
||||
if (buildOpts.settings.config.output === 'server') {
|
||||
|
@ -100,6 +119,20 @@ export async function generateImage(
|
|||
clientRoot = buildOpts.settings.config.outDir;
|
||||
}
|
||||
|
||||
const finalFileURL = new URL('.' + filepath, clientRoot);
|
||||
const finalFolderURL = new URL('./', finalFileURL);
|
||||
const cachedFileURL = new URL(basename(filepath), assetsCacheDir);
|
||||
|
||||
try {
|
||||
await fs.promises.copyFile(cachedFileURL, finalFileURL);
|
||||
|
||||
return {
|
||||
cached: true,
|
||||
};
|
||||
} catch (e) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
// The original file's path (the `src` attribute of the ESM imported image passed by the user)
|
||||
const originalImagePath = options.src.src;
|
||||
|
||||
|
@ -112,19 +145,33 @@ export async function generateImage(
|
|||
serverRoot
|
||||
)
|
||||
);
|
||||
|
||||
const imageService = (await getConfiguredImageService()) as LocalImageService;
|
||||
const resultData = await imageService.transform(
|
||||
fileData,
|
||||
{ ...options, src: originalImagePath },
|
||||
buildOpts.settings.config.image.service.config
|
||||
);
|
||||
|
||||
const finalFileURL = new URL('.' + filepath, clientRoot);
|
||||
const finalFolderURL = new URL('./', finalFileURL);
|
||||
|
||||
await fs.promises.mkdir(finalFolderURL, { recursive: true });
|
||||
|
||||
if (useCache) {
|
||||
try {
|
||||
await fs.promises.writeFile(cachedFileURL, resultData.data);
|
||||
await fs.promises.copyFile(cachedFileURL, finalFileURL);
|
||||
} catch (e) {
|
||||
console.error(
|
||||
`There was an error creating the cache entry for ${filepath}. Attempting to write directly to output directory. Error: `,
|
||||
e
|
||||
);
|
||||
await fs.promises.writeFile(finalFileURL, resultData.data);
|
||||
}
|
||||
} else {
|
||||
await fs.promises.writeFile(finalFileURL, resultData.data);
|
||||
}
|
||||
|
||||
return {
|
||||
cached: false,
|
||||
weight: {
|
||||
before: Math.trunc(fileData.byteLength / 1024),
|
||||
after: Math.trunc(resultData.data.byteLength / 1024),
|
||||
|
|
|
@ -146,13 +146,10 @@ async function generateImage(opts: StaticBuildOptions, transform: ImageTransform
|
|||
const timeEnd = performance.now();
|
||||
const timeChange = getTimeStat(timeStart, timeEnd);
|
||||
const timeIncrease = `(+${timeChange})`;
|
||||
info(
|
||||
opts.logging,
|
||||
null,
|
||||
` ${green('▶')} ${path} ${dim(
|
||||
`(before: ${generationData.weight.before}kb, after: ${generationData.weight.after}kb)`
|
||||
)} ${dim(timeIncrease)}`
|
||||
);
|
||||
const statsText = generationData.cached
|
||||
? `(reused cache entry)`
|
||||
: `(before: ${generationData.weight.before}kb, after: ${generationData.weight.after}kb)`;
|
||||
info(opts.logging, null, ` ${green('▶')} ${path} ${dim(statsText)} ${dim(timeIncrease)}`);
|
||||
}
|
||||
|
||||
async function generatePage(
|
||||
|
|
|
@ -13,6 +13,7 @@ const ASTRO_CONFIG_DEFAULTS: AstroUserConfig & any = {
|
|||
srcDir: './src',
|
||||
publicDir: './public',
|
||||
outDir: './dist',
|
||||
cacheDir: './node_modules/.astro',
|
||||
base: '/',
|
||||
trailingSlash: 'ignore',
|
||||
build: {
|
||||
|
@ -63,6 +64,11 @@ export const AstroConfigSchema = z.object({
|
|||
.optional()
|
||||
.default(ASTRO_CONFIG_DEFAULTS.outDir)
|
||||
.transform((val) => new URL(val)),
|
||||
cacheDir: z
|
||||
.string()
|
||||
.optional()
|
||||
.default(ASTRO_CONFIG_DEFAULTS.cacheDir)
|
||||
.transform((val) => new URL(val)),
|
||||
site: z.string().url().optional(),
|
||||
base: z.string().optional().default(ASTRO_CONFIG_DEFAULTS.base),
|
||||
trailingSlash: z
|
||||
|
@ -220,6 +226,10 @@ export function createRelativeSchema(cmd: string, fileProtocolRoot: URL) {
|
|||
.string()
|
||||
.default(ASTRO_CONFIG_DEFAULTS.outDir)
|
||||
.transform((val) => new URL(appendForwardSlash(val), fileProtocolRoot)),
|
||||
cacheDir: z
|
||||
.string()
|
||||
.default(ASTRO_CONFIG_DEFAULTS.cacheDir)
|
||||
.transform((val) => new URL(appendForwardSlash(val), fileProtocolRoot)),
|
||||
build: z
|
||||
.object({
|
||||
format: z
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import { expect } from 'chai';
|
||||
import * as cheerio from 'cheerio';
|
||||
import { basename } from 'node:path';
|
||||
import { Writable } from 'node:stream';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { removeDir } from '../dist/core/fs/index.js';
|
||||
import testAdapter from './test-adapter.js';
|
||||
import { loadFixture } from './test-utils.js';
|
||||
|
||||
|
@ -455,6 +457,9 @@ describe('astro:image', () => {
|
|||
assets: true,
|
||||
},
|
||||
});
|
||||
// Remove cache directory
|
||||
removeDir(new URL('./fixtures/core-image-ssg/node_modules/.astro', import.meta.url));
|
||||
|
||||
await fixture.build();
|
||||
});
|
||||
|
||||
|
@ -569,6 +574,39 @@ describe('astro:image', () => {
|
|||
const $ = cheerio.load(html);
|
||||
expect($('#no-format img').attr('src')).to.not.equal($('#format-avif img').attr('src'));
|
||||
});
|
||||
|
||||
it('has cache entries', async () => {
|
||||
const generatedImages = (await fixture.glob('_astro/**/*.webp')).map((path) =>
|
||||
basename(path)
|
||||
);
|
||||
const cachedImages = (await fixture.glob('../node_modules/.astro/assets/**/*.webp')).map(
|
||||
(path) => basename(path)
|
||||
);
|
||||
|
||||
expect(generatedImages).to.deep.equal(cachedImages);
|
||||
});
|
||||
|
||||
it('uses cache entries', async () => {
|
||||
const logs = [];
|
||||
const logging = {
|
||||
dest: {
|
||||
write(chunk) {
|
||||
logs.push(chunk);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
await fixture.build({ logging });
|
||||
const generatingImageIndex = logs.findIndex((logLine) =>
|
||||
logLine.message.includes('generating optimized images')
|
||||
);
|
||||
const relevantLogs = logs.slice(generatingImageIndex + 1, -1);
|
||||
const isReusingCache = relevantLogs.every((logLine) =>
|
||||
logLine.message.includes('(reused cache entry)')
|
||||
);
|
||||
|
||||
expect(isReusingCache).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe('prod ssr', () => {
|
||||
|
|
Loading…
Reference in a new issue