Skip to content

Commit 0a137f9

Browse files
authored
perf(@angular/build): share sass directory and resolution caches across stylesheets
Previously, Sass resolution caches (resolutionCache and packageRootCache) in sass-language.ts and the filesystem directory entry cache (directoryCache) in sass-service.ts were created anew for every individual stylesheet compilation request.
1 parent 482f6d8 commit 0a137f9

4 files changed

Lines changed: 56 additions & 15 deletions

File tree

packages/angular/build/src/builders/application/build-action.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@ import {
1515
RebuildState,
1616
} from '../../tools/esbuild/bundler-execution-result';
1717
import { BuildOutputFile, BuildOutputFileType } from '../../tools/esbuild/bundler-files';
18-
import { shutdownSassWorkerPool } from '../../tools/esbuild/stylesheets/sass-language';
18+
import {
19+
resetSassWorkerPoolCaches,
20+
shutdownSassWorkerPool,
21+
} from '../../tools/esbuild/stylesheets/sass-language';
1922
import { logMessages, withNoProgress, withSpinner } from '../../tools/esbuild/utils';
2023
import { ChangedFiles } from '../../tools/esbuild/watcher';
2124
import { shouldWatchRoot } from '../../utils/environment-options';
@@ -210,6 +213,8 @@ export async function* runEsBuildBuildAction(
210213
// Clear removed files from current watch files
211214
changes.removed.forEach((removedPath) => currentWatchFiles.delete(removedPath));
212215

216+
resetSassWorkerPoolCaches();
217+
213218
const rebuildState = result.createRebuildState(changes);
214219
result = await withProgress('Changes detected. Rebuilding...', () => action(rebuildState));
215220

packages/angular/build/src/tools/esbuild/stylesheets/sass-language.ts

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,25 @@ import { StylesheetLanguage, StylesheetPluginOptions } from './stylesheet-plugin
1616

1717
let sassService: SassCompiler | undefined;
1818
let sassServicePromise: Promise<SassCompiler> | undefined;
19+
let resolutionCache: MemoryCache<URL | null> | undefined;
20+
let packageRootCache: MemoryCache<string | null> | undefined;
1921

2022
function isSassException(error: unknown): error is Exception {
2123
return !!error && typeof error === 'object' && 'sassMessage' in error;
2224
}
2325

26+
export function resetSassWorkerPoolCaches(): void {
27+
resolutionCache?.clear();
28+
packageRootCache?.clear();
29+
if (sassService) {
30+
sassService.clearCache();
31+
} else if (sassServicePromise) {
32+
void sassServicePromise.then((service) => service.clearCache());
33+
}
34+
}
35+
2436
export function shutdownSassWorkerPool(): void {
37+
resetSassWorkerPoolCaches();
2538
if (sassService) {
2639
void sassService.close();
2740
sassService = undefined;
@@ -91,14 +104,15 @@ async function compileString(
91104
}
92105
}
93106

94-
// Cache is currently local to individual compile requests.
95-
// Caching follows Sass behavior where a given url will always resolve to the same value
96-
// regardless of its importer's path.
107+
// Caching follows Sass behavior where a given package url will always resolve to the same value
108+
// regardless of its importer's path. Relative paths are qualified with the containing URL.
97109
// A null value indicates that the cached resolution attempt failed to find a location and
98110
// later stage resolution should be attempted. This avoids potentially expensive repeat
99111
// failing resolution attempts.
100-
const resolutionCache = new MemoryCache<URL | null>();
101-
const packageRootCache = new MemoryCache<string | null>();
112+
resolutionCache ??= new MemoryCache<URL | null>();
113+
packageRootCache ??= new MemoryCache<string | null>();
114+
const currentResolutionCache = resolutionCache;
115+
const currentPackageRootCache = packageRootCache;
102116
const warnings: PartialMessage[] = [];
103117
const { silenceDeprecations, futureDeprecations, fatalDeprecations } = options.sass ?? {};
104118

@@ -116,8 +130,12 @@ async function compileString(
116130
quietDeps: true,
117131
importers: [
118132
{
119-
findFileUrl: (url, options) =>
120-
resolutionCache.getOrCreate(url, async () => {
133+
findFileUrl: (url, options) => {
134+
const cacheKey = url.startsWith('pkg:')
135+
? url
136+
: `${options.containingUrl?.href ?? ''}:${url}`;
137+
138+
return currentResolutionCache.getOrCreate(cacheKey, async () => {
121139
const result = await resolveUrl(url, options);
122140
if (result.path) {
123141
return pathToFileURL(result.path);
@@ -128,12 +146,16 @@ async function compileString(
128146

129147
// Caching package root locations is particularly beneficial for `@material/*` packages
130148
// which extensively use deep imports.
131-
const packageRoot = await packageRootCache.getOrCreate(packageName, async () => {
132-
// Use the required presence of a package root `package.json` file to resolve the location
133-
const packageResult = await resolveUrl(packageName + '/package.json', options);
149+
const packageRootKey = `${options.containingUrl?.href ?? ''}:${packageName}`;
150+
const packageRoot = await currentPackageRootCache.getOrCreate(
151+
packageRootKey,
152+
async () => {
153+
// Use the required presence of a package root `package.json` file to resolve the location
154+
const packageResult = await resolveUrl(packageName + '/package.json', options);
134155

135-
return packageResult.path ? dirname(packageResult.path) : null;
136-
});
156+
return packageResult.path ? dirname(packageResult.path) : null;
157+
},
158+
);
137159

138160
// Package not found could be because of an error or the specifier is intended to be found
139161
// via a later stage of the resolution process (`loadPaths`, etc.).
@@ -145,7 +167,8 @@ async function compileString(
145167

146168
// Not found
147169
return null;
148-
}),
170+
});
171+
},
149172
},
150173
],
151174
logger: {

packages/angular/build/src/tools/sass/sass-service.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ function isFileImporter(value: Importers): value is FileImporter {
4545
export class SassCompiler {
4646
#asyncCompiler: AsyncCompiler | undefined;
4747
#asyncCompilerPromise: Promise<AsyncCompiler> | undefined;
48+
readonly #directoryCache = new Map<string, DirectoryEntry>();
4849

4950
constructor(private readonly rebase = false) {}
5051

@@ -119,7 +120,7 @@ export class SassCompiler {
119120
(Importer<'async'> | FileImporter<'async'> | NodePackageImporter)[] | undefined;
120121
let loadPaths = options.loadPaths;
121122
const entryDirectory = url ? dirname(fileURLToPath(url)) : process.cwd();
122-
const directoryCache = new Map<string, DirectoryEntry>();
123+
const directoryCache = this.#directoryCache;
123124
const rebaseSourceMaps = options.sourceMap ? new Map<string, DecodedSourceMap>() : undefined;
124125

125126
if (importers?.length) {
@@ -187,11 +188,20 @@ export class SassCompiler {
187188
return result;
188189
}
189190

191+
/**
192+
* Clear the directory cache.
193+
*/
194+
clearCache(): void {
195+
this.#directoryCache.clear();
196+
}
197+
190198
/**
191199
* Shutdown the Sass compiler.
192200
* @returns A void promise that resolves when closing is complete.
193201
*/
194202
async close(): Promise<void> {
203+
this.clearCache();
204+
195205
if (this.#asyncCompilerPromise) {
196206
try {
197207
await this.#ensureAsyncCompiler();

packages/angular_devkit/build_angular/src/tools/webpack/configs/styles.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ export async function getStylesConfig(wco: WebpackConfigOptions): Promise<Config
7070

7171
extraPlugins.push({
7272
apply(compiler) {
73+
compiler.hooks.thisCompilation.tap('sass-service', () => {
74+
sassImplementation.clearCache();
75+
});
7376
compiler.hooks.shutdown.tap('sass-service', () => {
7477
void sassImplementation.close();
7578
});

0 commit comments

Comments
 (0)