Skip to content

Commit

Permalink
perf(@angular-devkit/build-angular): cache Sass in memory with esbuil…
Browse files Browse the repository at this point in the history
…d 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)
  • Loading branch information
clydin authored and dgp1130 committed Apr 13, 2023
1 parent ac3e10e commit a710a26
Show file tree
Hide file tree
Showing 6 changed files with 122 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { pathToFileURL } from 'node:url';
import ts from 'typescript';
import { maxWorkers } from '../../../utils/environment-options';
import { JavaScriptTransformer } from '../javascript-transformer';
import { LoadResultCache, MemoryLoadResultCache } from '../load-result-cache';
import {
logCumulativeDurations,
profileAsync,
Expand Down Expand Up @@ -124,12 +125,14 @@ export class SourceFileCache extends Map<string, ts.SourceFile> {
readonly modifiedFiles = new Set<string>();
readonly babelFileCache = new Map<string, Uint8Array>();
readonly typeScriptFileCache = new Map<string, Uint8Array>();
readonly loadResultCache = new MemoryLoadResultCache();

invalidate(files: Iterable<string>): void {
this.modifiedFiles.clear();
for (let file of files) {
this.babelFileCache.delete(file);
this.typeScriptFileCache.delete(pathToFileURL(file).href);
this.loadResultCache.invalidate(file);

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

// eslint-disable-next-line max-lines-per-function
Expand Down Expand Up @@ -272,6 +276,7 @@ export function createCompilerPlugin(
filename,
!stylesheetFile,
styleOptions,
pluginOptions.loadResultCache,
);

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

// Setup bundling of component templates and stylesheets when in JIT mode
if (pluginOptions.jit) {
setupJitPluginCallbacks(build, styleOptions, stylesheetResourceFiles);
setupJitPluginCallbacks(
build,
styleOptions,
stylesheetResourceFiles,
pluginOptions.loadResultCache,
);
}

build.onEnd((result) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import type { OutputFile, PluginBuild } from 'esbuild';
import { readFile } from 'node:fs/promises';
import path from 'node:path';
import { LoadResultCache } from '../load-result-cache';
import { BundleStylesheetOptions, bundleComponentStylesheet } from '../stylesheets';
import {
JIT_NAMESPACE_REGEXP,
Expand Down Expand Up @@ -65,6 +66,7 @@ export function setupJitPluginCallbacks(
build: PluginBuild,
styleOptions: BundleStylesheetOptions & { inlineStyleLanguage: string },
stylesheetResourceFiles: OutputFile[],
cache?: LoadResultCache,
): void {
const root = build.initialOptions.absWorkingDir ?? '';

Expand Down Expand Up @@ -110,6 +112,7 @@ export function setupJitPluginCallbacks(
entry.path,
entry.contents !== undefined,
styleOptions,
cache,
);

stylesheetResourceFiles.push(...resourceFiles);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { BundlerContext, logMessages } from './esbuild';
import { logExperimentalWarnings } from './experimental-warnings';
import { createGlobalScriptsBundleOptions } from './global-scripts';
import { extractLicenses } from './license-extractor';
import { LoadResultCache } from './load-result-cache';
import { NormalizedBrowserOptions, normalizeOptions } from './options';
import { shutdownSassWorkerPool } from './sass-plugin';
import { Schema as BrowserBuilderOptions } from './schema';
Expand Down Expand Up @@ -122,7 +123,7 @@ async function execute(
new BundlerContext(
workspaceRoot,
!!options.watch,
createGlobalStylesBundleOptions(options, target, browsers),
createGlobalStylesBundleOptions(options, target, browsers, codeBundleCache?.loadResultCache),
);

const globalScriptsBundleContext = new BundlerContext(
Expand Down Expand Up @@ -390,6 +391,7 @@ function createCodeBundleOptions(
advancedOptimizations,
fileReplacements,
sourceFileCache,
loadResultCache: sourceFileCache?.loadResultCache,
},
// Component stylesheet options
{
Expand Down Expand Up @@ -508,6 +510,7 @@ function createGlobalStylesBundleOptions(
options: NormalizedBrowserOptions,
target: string[],
browsers: string[],
cache?: LoadResultCache,
): BuildOptions {
const {
workspaceRoot,
Expand All @@ -521,18 +524,21 @@ function createGlobalStylesBundleOptions(
tailwindConfiguration,
} = options;

const buildOptions = createStylesheetBundleOptions({
workspaceRoot,
optimization: !!optimizationOptions.styles.minify,
sourcemap: !!sourcemapOptions.styles,
preserveSymlinks,
target,
externalDependencies,
outputNames,
includePaths: stylePreprocessorOptions?.includePaths,
browsers,
tailwindConfiguration,
});
const buildOptions = createStylesheetBundleOptions(
{
workspaceRoot,
optimization: !!optimizationOptions.styles.minify,
sourcemap: !!sourcemapOptions.styles,
preserveSymlinks,
target,
externalDependencies,
outputNames,
includePaths: stylePreprocessorOptions?.includePaths,
browsers,
tailwindConfiguration,
},
cache,
);
buildOptions.legalComments = options.extractLicenses ? 'none' : 'eof';

const namespace = 'angular:styles/global';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import type { OnLoadResult } from 'esbuild';

export interface LoadResultCache {
get(path: string): OnLoadResult | undefined;
put(path: string, result: OnLoadResult): Promise<void>;
}

export class MemoryLoadResultCache implements LoadResultCache {
#loadResults = new Map<string, OnLoadResult>();
#fileDependencies = new Map<string, Set<string>>();

get(path: string): OnLoadResult | undefined {
return this.#loadResults.get(path);
}

async put(path: string, result: OnLoadResult): Promise<void> {
this.#loadResults.set(path, result);
if (result.watchFiles) {
for (const watchFile of result.watchFiles) {
let affected = this.#fileDependencies.get(watchFile);
if (affected === undefined) {
affected = new Set();
this.#fileDependencies.set(watchFile, affected);
}
affected.add(path);
}
}
}

invalidate(path: string): boolean {
const affected = this.#fileDependencies.get(path);
let found = false;

if (affected) {
affected.forEach((a) => (found ||= this.#loadResults.delete(a)));
this.#fileDependencies.delete(path);
}

found ||= this.#loadResults.delete(path);

return found;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type {
FileImporterWithRequestContextOptions,
SassWorkerImplementation,
} from '../../sass/sass-service';
import type { LoadResultCache } from './load-result-cache';

export interface SassPluginOptions {
sourcemap: boolean;
Expand All @@ -34,7 +35,7 @@ export function shutdownSassWorkerPool(): void {
sassWorkerPool = undefined;
}

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

const [language, , filePath] = args.path.split(';', 3);
const syntax = language === 'sass' ? 'indented' : 'scss';
let result = cache?.get(data);
if (result === undefined) {
const [language, , filePath] = args.path.split(';', 3);
const syntax = language === 'sass' ? 'indented' : 'scss';

return compileString(data, filePath, syntax, options, resolveUrl);
result = await compileString(data, filePath, syntax, options, resolveUrl);
if (result.errors === undefined) {
// Cache the result if there were no errors
await cache?.put(data, result);
}
}

return result;
});

build.onLoad({ filter: /\.s[ac]ss$/ }, async (args) => {
const data = await readFile(args.path, 'utf-8');
const syntax = extname(args.path).toLowerCase() === '.sass' ? 'indented' : 'scss';
let result = cache?.get(args.path);
if (result === undefined) {
const data = await readFile(args.path, 'utf-8');
const syntax = extname(args.path).toLowerCase() === '.sass' ? 'indented' : 'scss';

result = await compileString(data, args.path, syntax, options, resolveUrl);
if (result.errors === undefined) {
// Cache the result if there were no errors
await cache?.put(args.path, result);
}
}

return compileString(data, args.path, syntax, options, resolveUrl);
return result;
});
},
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { createCssPlugin } from './css-plugin';
import { createCssResourcePlugin } from './css-resource-plugin';
import { BundlerContext } from './esbuild';
import { createLessPlugin } from './less-plugin';
import { LoadResultCache } from './load-result-cache';
import { createSassPlugin } from './sass-plugin';

/**
Expand All @@ -34,6 +35,7 @@ export interface BundleStylesheetOptions {

export function createStylesheetBundleOptions(
options: BundleStylesheetOptions,
cache?: LoadResultCache,
inlineComponentData?: Record<string, string>,
): BuildOptions & { plugins: NonNullable<BuildOptions['plugins']> } {
// Ensure preprocessor include paths are absolute based on the workspace root
Expand All @@ -59,11 +61,14 @@ export function createStylesheetBundleOptions(
conditions: ['style', 'sass'],
mainFields: ['style', 'sass'],
plugins: [
createSassPlugin({
sourcemap: !!options.sourcemap,
loadPaths: includePaths,
inlineComponentData,
}),
createSassPlugin(
{
sourcemap: !!options.sourcemap,
loadPaths: includePaths,
inlineComponentData,
},
cache,
),
createLessPlugin({
sourcemap: !!options.sourcemap,
includePaths,
Expand Down Expand Up @@ -100,11 +105,12 @@ export async function bundleComponentStylesheet(
filename: string,
inline: boolean,
options: BundleStylesheetOptions,
cache?: LoadResultCache,
) {
const namespace = 'angular:styles/component';
const entry = [language, componentStyleCounter++, filename].join(';');

const buildOptions = createStylesheetBundleOptions(options, { [entry]: data });
const buildOptions = createStylesheetBundleOptions(options, cache, { [entry]: data });
buildOptions.entryPoints = [`${namespace};${entry}`];
buildOptions.plugins.push({
name: 'angular-component-styles',
Expand Down

0 comments on commit a710a26

Please sign in to comment.