Skip to content

Commit a710a26

Browse files
clydindgp1130
authored andcommitted
perf(@angular-devkit/build-angular): cache Sass in memory with esbuild watch mode
To improve rebuild performance when using Sass stylesheets with the esbuild-based browser application builder in watch mode, Sass stylesheets that are not affected by any file changes will now be cached and directly reused. This avoids performing potentially expensive Sass preprocessing on stylesheets that will not change within a rebuild. (cherry picked from commit 1e78cf9)
1 parent ac3e10e commit a710a26

File tree

6 files changed

+122
-27
lines changed

6 files changed

+122
-27
lines changed

packages/angular_devkit/build_angular/src/builders/browser-esbuild/angular/compiler-plugin.ts

+11-1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { pathToFileURL } from 'node:url';
2222
import ts from 'typescript';
2323
import { maxWorkers } from '../../../utils/environment-options';
2424
import { JavaScriptTransformer } from '../javascript-transformer';
25+
import { LoadResultCache, MemoryLoadResultCache } from '../load-result-cache';
2526
import {
2627
logCumulativeDurations,
2728
profileAsync,
@@ -124,12 +125,14 @@ export class SourceFileCache extends Map<string, ts.SourceFile> {
124125
readonly modifiedFiles = new Set<string>();
125126
readonly babelFileCache = new Map<string, Uint8Array>();
126127
readonly typeScriptFileCache = new Map<string, Uint8Array>();
128+
readonly loadResultCache = new MemoryLoadResultCache();
127129

128130
invalidate(files: Iterable<string>): void {
129131
this.modifiedFiles.clear();
130132
for (let file of files) {
131133
this.babelFileCache.delete(file);
132134
this.typeScriptFileCache.delete(pathToFileURL(file).href);
135+
this.loadResultCache.invalidate(file);
133136

134137
// Normalize separators to allow matching TypeScript Host paths
135138
if (USING_WINDOWS) {
@@ -150,6 +153,7 @@ export interface CompilerPluginOptions {
150153
thirdPartySourcemaps?: boolean;
151154
fileReplacements?: Record<string, string>;
152155
sourceFileCache?: SourceFileCache;
156+
loadResultCache?: LoadResultCache;
153157
}
154158

155159
// eslint-disable-next-line max-lines-per-function
@@ -272,6 +276,7 @@ export function createCompilerPlugin(
272276
filename,
273277
!stylesheetFile,
274278
styleOptions,
279+
pluginOptions.loadResultCache,
275280
);
276281

277282
const { contents, resourceFiles, errors, warnings } = stylesheetResult;
@@ -415,7 +420,12 @@ export function createCompilerPlugin(
415420

416421
// Setup bundling of component templates and stylesheets when in JIT mode
417422
if (pluginOptions.jit) {
418-
setupJitPluginCallbacks(build, styleOptions, stylesheetResourceFiles);
423+
setupJitPluginCallbacks(
424+
build,
425+
styleOptions,
426+
stylesheetResourceFiles,
427+
pluginOptions.loadResultCache,
428+
);
419429
}
420430

421431
build.onEnd((result) => {

packages/angular_devkit/build_angular/src/builders/browser-esbuild/angular/jit-plugin-callbacks.ts

+3
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import type { OutputFile, PluginBuild } from 'esbuild';
1010
import { readFile } from 'node:fs/promises';
1111
import path from 'node:path';
12+
import { LoadResultCache } from '../load-result-cache';
1213
import { BundleStylesheetOptions, bundleComponentStylesheet } from '../stylesheets';
1314
import {
1415
JIT_NAMESPACE_REGEXP,
@@ -65,6 +66,7 @@ export function setupJitPluginCallbacks(
6566
build: PluginBuild,
6667
styleOptions: BundleStylesheetOptions & { inlineStyleLanguage: string },
6768
stylesheetResourceFiles: OutputFile[],
69+
cache?: LoadResultCache,
6870
): void {
6971
const root = build.initialOptions.absWorkingDir ?? '';
7072

@@ -110,6 +112,7 @@ export function setupJitPluginCallbacks(
110112
entry.path,
111113
entry.contents !== undefined,
112114
styleOptions,
115+
cache,
113116
);
114117

115118
stylesheetResourceFiles.push(...resourceFiles);

packages/angular_devkit/build_angular/src/builders/browser-esbuild/index.ts

+19-13
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { BundlerContext, logMessages } from './esbuild';
2727
import { logExperimentalWarnings } from './experimental-warnings';
2828
import { createGlobalScriptsBundleOptions } from './global-scripts';
2929
import { extractLicenses } from './license-extractor';
30+
import { LoadResultCache } from './load-result-cache';
3031
import { NormalizedBrowserOptions, normalizeOptions } from './options';
3132
import { shutdownSassWorkerPool } from './sass-plugin';
3233
import { Schema as BrowserBuilderOptions } from './schema';
@@ -122,7 +123,7 @@ async function execute(
122123
new BundlerContext(
123124
workspaceRoot,
124125
!!options.watch,
125-
createGlobalStylesBundleOptions(options, target, browsers),
126+
createGlobalStylesBundleOptions(options, target, browsers, codeBundleCache?.loadResultCache),
126127
);
127128

128129
const globalScriptsBundleContext = new BundlerContext(
@@ -390,6 +391,7 @@ function createCodeBundleOptions(
390391
advancedOptimizations,
391392
fileReplacements,
392393
sourceFileCache,
394+
loadResultCache: sourceFileCache?.loadResultCache,
393395
},
394396
// Component stylesheet options
395397
{
@@ -508,6 +510,7 @@ function createGlobalStylesBundleOptions(
508510
options: NormalizedBrowserOptions,
509511
target: string[],
510512
browsers: string[],
513+
cache?: LoadResultCache,
511514
): BuildOptions {
512515
const {
513516
workspaceRoot,
@@ -521,18 +524,21 @@ function createGlobalStylesBundleOptions(
521524
tailwindConfiguration,
522525
} = options;
523526

524-
const buildOptions = createStylesheetBundleOptions({
525-
workspaceRoot,
526-
optimization: !!optimizationOptions.styles.minify,
527-
sourcemap: !!sourcemapOptions.styles,
528-
preserveSymlinks,
529-
target,
530-
externalDependencies,
531-
outputNames,
532-
includePaths: stylePreprocessorOptions?.includePaths,
533-
browsers,
534-
tailwindConfiguration,
535-
});
527+
const buildOptions = createStylesheetBundleOptions(
528+
{
529+
workspaceRoot,
530+
optimization: !!optimizationOptions.styles.minify,
531+
sourcemap: !!sourcemapOptions.styles,
532+
preserveSymlinks,
533+
target,
534+
externalDependencies,
535+
outputNames,
536+
includePaths: stylePreprocessorOptions?.includePaths,
537+
browsers,
538+
tailwindConfiguration,
539+
},
540+
cache,
541+
);
536542
buildOptions.legalComments = options.extractLicenses ? 'none' : 'eof';
537543

538544
const namespace = 'angular:styles/global';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import type { OnLoadResult } from 'esbuild';
10+
11+
export interface LoadResultCache {
12+
get(path: string): OnLoadResult | undefined;
13+
put(path: string, result: OnLoadResult): Promise<void>;
14+
}
15+
16+
export class MemoryLoadResultCache implements LoadResultCache {
17+
#loadResults = new Map<string, OnLoadResult>();
18+
#fileDependencies = new Map<string, Set<string>>();
19+
20+
get(path: string): OnLoadResult | undefined {
21+
return this.#loadResults.get(path);
22+
}
23+
24+
async put(path: string, result: OnLoadResult): Promise<void> {
25+
this.#loadResults.set(path, result);
26+
if (result.watchFiles) {
27+
for (const watchFile of result.watchFiles) {
28+
let affected = this.#fileDependencies.get(watchFile);
29+
if (affected === undefined) {
30+
affected = new Set();
31+
this.#fileDependencies.set(watchFile, affected);
32+
}
33+
affected.add(path);
34+
}
35+
}
36+
}
37+
38+
invalidate(path: string): boolean {
39+
const affected = this.#fileDependencies.get(path);
40+
let found = false;
41+
42+
if (affected) {
43+
affected.forEach((a) => (found ||= this.#loadResults.delete(a)));
44+
this.#fileDependencies.delete(path);
45+
}
46+
47+
found ||= this.#loadResults.delete(path);
48+
49+
return found;
50+
}
51+
}

packages/angular_devkit/build_angular/src/builders/browser-esbuild/sass-plugin.ts

+26-7
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import type {
1616
FileImporterWithRequestContextOptions,
1717
SassWorkerImplementation,
1818
} from '../../sass/sass-service';
19+
import type { LoadResultCache } from './load-result-cache';
1920

2021
export interface SassPluginOptions {
2122
sourcemap: boolean;
@@ -34,7 +35,7 @@ export function shutdownSassWorkerPool(): void {
3435
sassWorkerPool = undefined;
3536
}
3637

37-
export function createSassPlugin(options: SassPluginOptions): Plugin {
38+
export function createSassPlugin(options: SassPluginOptions, cache?: LoadResultCache): Plugin {
3839
return {
3940
name: 'angular-sass',
4041
setup(build: PluginBuild): void {
@@ -69,17 +70,35 @@ export function createSassPlugin(options: SassPluginOptions): Plugin {
6970
`component style name should always be found [${args.path}]`,
7071
);
7172

72-
const [language, , filePath] = args.path.split(';', 3);
73-
const syntax = language === 'sass' ? 'indented' : 'scss';
73+
let result = cache?.get(data);
74+
if (result === undefined) {
75+
const [language, , filePath] = args.path.split(';', 3);
76+
const syntax = language === 'sass' ? 'indented' : 'scss';
7477

75-
return compileString(data, filePath, syntax, options, resolveUrl);
78+
result = await compileString(data, filePath, syntax, options, resolveUrl);
79+
if (result.errors === undefined) {
80+
// Cache the result if there were no errors
81+
await cache?.put(data, result);
82+
}
83+
}
84+
85+
return result;
7686
});
7787

7888
build.onLoad({ filter: /\.s[ac]ss$/ }, async (args) => {
79-
const data = await readFile(args.path, 'utf-8');
80-
const syntax = extname(args.path).toLowerCase() === '.sass' ? 'indented' : 'scss';
89+
let result = cache?.get(args.path);
90+
if (result === undefined) {
91+
const data = await readFile(args.path, 'utf-8');
92+
const syntax = extname(args.path).toLowerCase() === '.sass' ? 'indented' : 'scss';
93+
94+
result = await compileString(data, args.path, syntax, options, resolveUrl);
95+
if (result.errors === undefined) {
96+
// Cache the result if there were no errors
97+
await cache?.put(args.path, result);
98+
}
99+
}
81100

82-
return compileString(data, args.path, syntax, options, resolveUrl);
101+
return result;
83102
});
84103
},
85104
};

packages/angular_devkit/build_angular/src/builders/browser-esbuild/stylesheets.ts

+12-6
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { createCssPlugin } from './css-plugin';
1212
import { createCssResourcePlugin } from './css-resource-plugin';
1313
import { BundlerContext } from './esbuild';
1414
import { createLessPlugin } from './less-plugin';
15+
import { LoadResultCache } from './load-result-cache';
1516
import { createSassPlugin } from './sass-plugin';
1617

1718
/**
@@ -34,6 +35,7 @@ export interface BundleStylesheetOptions {
3435

3536
export function createStylesheetBundleOptions(
3637
options: BundleStylesheetOptions,
38+
cache?: LoadResultCache,
3739
inlineComponentData?: Record<string, string>,
3840
): BuildOptions & { plugins: NonNullable<BuildOptions['plugins']> } {
3941
// Ensure preprocessor include paths are absolute based on the workspace root
@@ -59,11 +61,14 @@ export function createStylesheetBundleOptions(
5961
conditions: ['style', 'sass'],
6062
mainFields: ['style', 'sass'],
6163
plugins: [
62-
createSassPlugin({
63-
sourcemap: !!options.sourcemap,
64-
loadPaths: includePaths,
65-
inlineComponentData,
66-
}),
64+
createSassPlugin(
65+
{
66+
sourcemap: !!options.sourcemap,
67+
loadPaths: includePaths,
68+
inlineComponentData,
69+
},
70+
cache,
71+
),
6772
createLessPlugin({
6873
sourcemap: !!options.sourcemap,
6974
includePaths,
@@ -100,11 +105,12 @@ export async function bundleComponentStylesheet(
100105
filename: string,
101106
inline: boolean,
102107
options: BundleStylesheetOptions,
108+
cache?: LoadResultCache,
103109
) {
104110
const namespace = 'angular:styles/component';
105111
const entry = [language, componentStyleCounter++, filename].join(';');
106112

107-
const buildOptions = createStylesheetBundleOptions(options, { [entry]: data });
113+
const buildOptions = createStylesheetBundleOptions(options, cache, { [entry]: data });
108114
buildOptions.entryPoints = [`${namespace};${entry}`];
109115
buildOptions.plugins.push({
110116
name: 'angular-component-styles',

0 commit comments

Comments
 (0)