Skip to content

Commit

Permalink
Add caching for optimized images (#6990)
Browse files Browse the repository at this point in the history
  • Loading branch information
Princesseuh authored May 4, 2023
1 parent 14fd198 commit 818252a
Show file tree
Hide file tree
Showing 6 changed files with 127 additions and 13 deletions.
5 changes: 5 additions & 0 deletions .changeset/forty-horses-act.md
Original file line number Diff line number Diff line change
@@ -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.
17 changes: 17 additions & 0 deletions packages/astro/src/@types/astro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
59 changes: 53 additions & 6 deletions packages/astro/src/assets/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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') {
Expand All @@ -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;

Expand All @@ -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 });
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 {
cached: false,
weight: {
before: Math.trunc(fileData.byteLength / 1024),
after: Math.trunc(resultData.data.byteLength / 1024),
Expand Down
11 changes: 4 additions & 7 deletions packages/astro/src/core/build/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
10 changes: 10 additions & 0 deletions packages/astro/src/core/config/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const ASTRO_CONFIG_DEFAULTS: AstroUserConfig & any = {
srcDir: './src',
publicDir: './public',
outDir: './dist',
cacheDir: './node_modules/.astro',
base: '/',
trailingSlash: 'ignore',
build: {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
38 changes: 38 additions & 0 deletions packages/astro/test/core-image.test.js
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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();
});

Expand Down Expand Up @@ -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', () => {
Expand Down

0 comments on commit 818252a

Please sign in to comment.