Skip to content

Commit f90a832

Browse files
alan-agius4clydin
authored andcommitted
perf(@angular-devkit/build-angular): enable opt-in usage of file system cache
With this change we enable Webpack's filesystem cache, this important because `terser-webpack-plugin`, `css-minimizer-webpack-plugin` and `copy-webpack-plugin` all use Webpacks' caching API to avoid additional processing during the 2nd cold build. This changes causes `node_modules` to be treated as immutable. Webpack will avoid hashing and timestamping them, assume the version is unique and will use it as a snapshot. To opt-in using the experimental persistent build cache use the`NG_PERSISTENT_BUILD_CACHE` environment variable. ``` NG_PERSISTENT_BUILD_CACHE=1 ng serve ```
1 parent 29f65c9 commit f90a832

File tree

4 files changed

+72
-19
lines changed

4 files changed

+72
-19
lines changed

packages/angular_devkit/build_angular/src/app-shell/index.ts

+8-8
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,8 @@ async function _renderUniversal(
7575

7676
const { AppServerModule, renderModule } = await import(serverBundlePath);
7777

78-
const renderModuleFn:
79-
| ((module: unknown, options: {}) => Promise<string>)
80-
| undefined = renderModule;
78+
const renderModuleFn: ((module: unknown, options: {}) => Promise<string>) | undefined =
79+
renderModule;
8180

8281
if (!(renderModuleFn && AppServerModule)) {
8382
throw new Error(
@@ -172,7 +171,7 @@ async function _appShellBuilder(
172171
const browserTargetRun = await context.scheduleTarget(browserTarget, {
173172
watch: false,
174173
serviceWorker: false,
175-
optimization: (optimization as unknown) as JsonObject,
174+
optimization: optimization as unknown as JsonObject,
176175
});
177176
const serverTargetRun = await context.scheduleTarget(serverTarget, {
178177
watch: false,
@@ -181,9 +180,10 @@ async function _appShellBuilder(
181180
let spinner: Spinner | undefined;
182181

183182
try {
183+
// Using `.result` instead of `.output` causes Webpack FS cache not to be created.
184184
const [browserResult, serverResult] = await Promise.all([
185-
(browserTargetRun.result as unknown) as BrowserBuilderOutput,
186-
(serverTargetRun.result as unknown) as ServerBuilderOutput,
185+
browserTargetRun.output.toPromise() as Promise<BrowserBuilderOutput>,
186+
serverTargetRun.output.toPromise() as Promise<ServerBuilderOutput>,
187187
]);
188188

189189
if (browserResult.success === false || browserResult.baseOutputPath === undefined) {
@@ -203,8 +203,8 @@ async function _appShellBuilder(
203203

204204
return { success: false, error: err.message };
205205
} finally {
206-
// Just be good citizens and stop those jobs.
207-
await Promise.all([browserTargetRun.stop(), serverTargetRun.stop()]);
206+
// workaround for [tsetse] All Promises in async functions must either be awaited or used in an expression.
207+
const _ = Promise.all([browserTargetRun.stop(), serverTargetRun.stop()]);
208208
}
209209
}
210210

packages/angular_devkit/build_angular/src/utils/environment-options.ts

+7
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,13 @@ export const cachingBasePath = (() => {
8080
return cacheVariable;
8181
})();
8282

83+
// Persistent build cache
84+
const persistentBuildCacheVariable = process.env['NG_PERSISTENT_BUILD_CACHE'];
85+
export const persistentBuildCacheEnabled =
86+
!cachingDisabled &&
87+
isPresent(persistentBuildCacheVariable) &&
88+
isEnabled(persistentBuildCacheVariable);
89+
8390
// Build profiling
8491
const profilingVariable = process.env['NG_BUILD_PROFILING'];
8592
export const profilingEnabled = isPresent(profilingVariable) && isEnabled(profilingVariable);

packages/angular_devkit/build_angular/src/webpack/configs/common.ts

+44-10
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,13 @@ import {
1010
BuildOptimizerWebpackPlugin,
1111
buildOptimizerLoaderPath,
1212
} from '@angular-devkit/build-optimizer';
13+
import {
14+
GLOBAL_DEFS_FOR_TERSER,
15+
GLOBAL_DEFS_FOR_TERSER_WITH_AOT,
16+
VERSION as NG_VERSION,
17+
} from '@angular/compiler-cli';
1318
import * as CopyWebpackPlugin from 'copy-webpack-plugin';
19+
import { createHash } from 'crypto';
1420
import { createWriteStream, existsSync, promises as fsPromises } from 'fs';
1521
import * as path from 'path';
1622
import { ScriptTarget } from 'typescript';
@@ -20,6 +26,7 @@ import {
2026
ContextReplacementPlugin,
2127
ProgressPlugin,
2228
RuleSetRule,
29+
WebpackOptionsNormalized,
2330
debug,
2431
} from 'webpack';
2532
import { AssetPatternClass } from '../../browser/schema';
@@ -31,6 +38,7 @@ import {
3138
allowMinify,
3239
cachingDisabled,
3340
maxWorkers,
41+
persistentBuildCacheEnabled,
3442
profilingEnabled,
3543
shouldBeautify,
3644
} from '../../utils/environment-options';
@@ -309,11 +317,6 @@ export function getCommonConfig(wco: WebpackConfigOptions): Configuration {
309317

310318
if (scriptsOptimization) {
311319
const TerserPlugin = require('terser-webpack-plugin');
312-
const {
313-
GLOBAL_DEFS_FOR_TERSER,
314-
GLOBAL_DEFS_FOR_TERSER_WITH_AOT,
315-
} = require('@angular/compiler-cli');
316-
317320
const angularGlobalDefinitions = buildOptions.aot
318321
? GLOBAL_DEFS_FOR_TERSER_WITH_AOT
319322
: GLOBAL_DEFS_FOR_TERSER;
@@ -480,11 +483,7 @@ export function getCommonConfig(wco: WebpackConfigOptions): Configuration {
480483
syncWebAssembly: true,
481484
asyncWebAssembly: true,
482485
},
483-
cache: !!buildOptions.watch &&
484-
!cachingDisabled && {
485-
type: 'memory',
486-
maxGenerations: 1,
487-
},
486+
cache: getCacheSettings(wco, buildBrowserFeatures.supportedBrowsers),
488487
optimization: {
489488
minimizer: extraMinimizers,
490489
moduleIds: 'deterministic',
@@ -504,3 +503,38 @@ export function getCommonConfig(wco: WebpackConfigOptions): Configuration {
504503
],
505504
};
506505
}
506+
507+
function getCacheSettings(
508+
wco: WebpackConfigOptions,
509+
supportedBrowsers: string[],
510+
): WebpackOptionsNormalized['cache'] {
511+
if (persistentBuildCacheEnabled) {
512+
const packageVersion = require('../../../package.json').version;
513+
514+
return {
515+
type: 'filesystem',
516+
cacheDirectory: findCachePath('angular-webpack'),
517+
maxMemoryGenerations: 1,
518+
// We use the versions and build options as the cache name. The Webpack configurations are too
519+
// dynamic and shared among different build types: test, build and serve.
520+
// None of which are "named".
521+
name: createHash('sha1')
522+
.update(NG_VERSION.full)
523+
.update(packageVersion)
524+
.update(wco.projectRoot)
525+
.update(JSON.stringify(wco.tsConfig))
526+
.update(JSON.stringify(wco.buildOptions))
527+
.update(supportedBrowsers.join(''))
528+
.digest('base64'),
529+
};
530+
}
531+
532+
if (wco.buildOptions.watch && !cachingDisabled) {
533+
return {
534+
type: 'memory',
535+
maxGenerations: 1,
536+
};
537+
}
538+
539+
return false;
540+
}

packages/angular_devkit/build_angular/src/webpack/utils/stats.ts

+13-1
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,12 @@ function generateBuildStats(hash: string, time: number, colors: boolean): string
160160
return `Build at: ${w(new Date().toISOString())} - Hash: ${w(hash)} - Time: ${w('' + time)}ms`;
161161
}
162162

163+
// We use this cache because we can have multiple builders running in the same process,
164+
// where each builder has different output path.
165+
166+
// Ideally, we should create the logging callback as a factory, but that would need a refactoring.
167+
const runsCache = new Set<string>();
168+
163169
function statsToString(
164170
json: StatsCompilation,
165171
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -176,8 +182,12 @@ function statsToString(
176182
const changedChunksStats: BundleStats[] = bundleState ?? [];
177183
let unchangedChunkNumber = 0;
178184
if (!bundleState?.length) {
185+
const isFirstRun = !runsCache.has(json.outputPath || '');
186+
179187
for (const chunk of json.chunks) {
180-
if (!chunk.rendered) {
188+
// During first build we want to display unchanged chunks
189+
// but unchanged cached chunks are always marked as not rendered.
190+
if (!isFirstRun && !chunk.rendered) {
181191
continue;
182192
}
183193

@@ -188,6 +198,8 @@ function statsToString(
188198
changedChunksStats.push(generateBundleStats({ ...chunk, size: summedSize }));
189199
}
190200
unchangedChunkNumber = json.chunks.length - changedChunksStats.length;
201+
202+
runsCache.add(json.outputPath || '');
191203
}
192204

193205
// Sort chunks by size in descending order

0 commit comments

Comments
 (0)