Add caching for optimized images (#6990)

This commit is contained in:
Erika 2023-05-04 17:49:55 +02:00 committed by GitHub
parent 14fd198ea5
commit 818252acda
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 127 additions and 13 deletions

View 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.

View file

@ -430,6 +430,23 @@ export interface AstroUserConfig {
*/ */
outDir?: string; 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 * @docs
* @name site * @name site

View file

@ -73,13 +73,20 @@ export function getStaticImageList(): Iterable<
return globalThis.astroAsset.staticImages?.entries(); return globalThis.astroAsset.staticImages?.entries();
} }
interface GenerationData { interface GenerationDataUncached {
cached: false;
weight: { weight: {
before: number; before: number;
after: number; after: number;
}; };
} }
interface GenerationDataCached {
cached: true;
}
type GenerationData = GenerationDataUncached | GenerationDataCached;
export async function generateImage( export async function generateImage(
buildOpts: StaticBuildOptions, buildOpts: StaticBuildOptions,
options: ImageTransform, options: ImageTransform,
@ -89,7 +96,19 @@ export async function generateImage(
return undefined; 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; let serverRoot: URL, clientRoot: URL;
if (buildOpts.settings.config.output === 'server') { if (buildOpts.settings.config.output === 'server') {
@ -100,6 +119,20 @@ export async function generateImage(
clientRoot = buildOpts.settings.config.outDir; 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) // The original file's path (the `src` attribute of the ESM imported image passed by the user)
const originalImagePath = options.src.src; const originalImagePath = options.src.src;
@ -112,19 +145,33 @@ export async function generateImage(
serverRoot serverRoot
) )
); );
const imageService = (await getConfiguredImageService()) as LocalImageService;
const resultData = await imageService.transform( const resultData = await imageService.transform(
fileData, fileData,
{ ...options, src: originalImagePath }, { ...options, src: originalImagePath },
buildOpts.settings.config.image.service.config buildOpts.settings.config.image.service.config
); );
const finalFileURL = new URL('.' + filepath, clientRoot);
const finalFolderURL = new URL('./', finalFileURL);
await fs.promises.mkdir(finalFolderURL, { recursive: true }); await fs.promises.mkdir(finalFolderURL, { recursive: true });
await fs.promises.writeFile(finalFileURL, resultData.data);
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 { return {
cached: false,
weight: { weight: {
before: Math.trunc(fileData.byteLength / 1024), before: Math.trunc(fileData.byteLength / 1024),
after: Math.trunc(resultData.data.byteLength / 1024), after: Math.trunc(resultData.data.byteLength / 1024),

View file

@ -146,13 +146,10 @@ async function generateImage(opts: StaticBuildOptions, transform: ImageTransform
const timeEnd = performance.now(); const timeEnd = performance.now();
const timeChange = getTimeStat(timeStart, timeEnd); const timeChange = getTimeStat(timeStart, timeEnd);
const timeIncrease = `(+${timeChange})`; const timeIncrease = `(+${timeChange})`;
info( const statsText = generationData.cached
opts.logging, ? `(reused cache entry)`
null, : `(before: ${generationData.weight.before}kb, after: ${generationData.weight.after}kb)`;
` ${green('▶')} ${path} ${dim( info(opts.logging, null, ` ${green('▶')} ${path} ${dim(statsText)} ${dim(timeIncrease)}`);
`(before: ${generationData.weight.before}kb, after: ${generationData.weight.after}kb)`
)} ${dim(timeIncrease)}`
);
} }
async function generatePage( async function generatePage(

View file

@ -13,6 +13,7 @@ const ASTRO_CONFIG_DEFAULTS: AstroUserConfig & any = {
srcDir: './src', srcDir: './src',
publicDir: './public', publicDir: './public',
outDir: './dist', outDir: './dist',
cacheDir: './node_modules/.astro',
base: '/', base: '/',
trailingSlash: 'ignore', trailingSlash: 'ignore',
build: { build: {
@ -63,6 +64,11 @@ export const AstroConfigSchema = z.object({
.optional() .optional()
.default(ASTRO_CONFIG_DEFAULTS.outDir) .default(ASTRO_CONFIG_DEFAULTS.outDir)
.transform((val) => new URL(val)), .transform((val) => new URL(val)),
cacheDir: z
.string()
.optional()
.default(ASTRO_CONFIG_DEFAULTS.cacheDir)
.transform((val) => new URL(val)),
site: z.string().url().optional(), site: z.string().url().optional(),
base: z.string().optional().default(ASTRO_CONFIG_DEFAULTS.base), base: z.string().optional().default(ASTRO_CONFIG_DEFAULTS.base),
trailingSlash: z trailingSlash: z
@ -220,6 +226,10 @@ export function createRelativeSchema(cmd: string, fileProtocolRoot: URL) {
.string() .string()
.default(ASTRO_CONFIG_DEFAULTS.outDir) .default(ASTRO_CONFIG_DEFAULTS.outDir)
.transform((val) => new URL(appendForwardSlash(val), fileProtocolRoot)), .transform((val) => new URL(appendForwardSlash(val), fileProtocolRoot)),
cacheDir: z
.string()
.default(ASTRO_CONFIG_DEFAULTS.cacheDir)
.transform((val) => new URL(appendForwardSlash(val), fileProtocolRoot)),
build: z build: z
.object({ .object({
format: z format: z

View file

@ -1,7 +1,9 @@
import { expect } from 'chai'; import { expect } from 'chai';
import * as cheerio from 'cheerio'; import * as cheerio from 'cheerio';
import { basename } from 'node:path';
import { Writable } from 'node:stream'; import { Writable } from 'node:stream';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import { removeDir } from '../dist/core/fs/index.js';
import testAdapter from './test-adapter.js'; import testAdapter from './test-adapter.js';
import { loadFixture } from './test-utils.js'; import { loadFixture } from './test-utils.js';
@ -455,6 +457,9 @@ describe('astro:image', () => {
assets: true, assets: true,
}, },
}); });
// Remove cache directory
removeDir(new URL('./fixtures/core-image-ssg/node_modules/.astro', import.meta.url));
await fixture.build(); await fixture.build();
}); });
@ -569,6 +574,39 @@ describe('astro:image', () => {
const $ = cheerio.load(html); const $ = cheerio.load(html);
expect($('#no-format img').attr('src')).to.not.equal($('#format-avif img').attr('src')); 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', () => { describe('prod ssr', () => {