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;
|
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
|
||||||
|
|
|
@ -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 });
|
||||||
|
|
||||||
|
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);
|
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),
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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', () => {
|
||||||
|
|
Loading…
Reference in a new issue