Skip to content

Commit ecfc614

Browse files
committed
refactor(@angular/build): use xxhash-wasm for fast non-cryptographic content hashing
Replace OpenSSL SHA-256 with xxhash-wasm for internal non-cryptographic hashing across the build system. This includes JavaScript transformer cache keys, persistent load result disk caches, incremental TypeScript compiler source file versioning, i18n inlining cache keys, stylesheet compilation configuration hashes, and dev server asset ETags. By utilizing a 64-bit non-cryptographic hash backed by WebAssembly, hashing throughput is significantly increased while eliminating V8 OpenSSL C++ context allocations and garbage collection churn during cold and incremental builds.
1 parent 5ee5420 commit ecfc614

20 files changed

Lines changed: 242 additions & 91 deletions

packages/angular/build/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ ts_project(
109109
":node_modules/vite",
110110
":node_modules/vitest",
111111
":node_modules/watchpack",
112+
":node_modules/xxhash-wasm",
112113
"//:node_modules/@angular/common",
113114
"//:node_modules/@angular/compiler",
114115
"//:node_modules/@angular/compiler-cli",

packages/angular/build/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@
4141
"source-map-support": "0.5.21",
4242
"tinyglobby": "0.2.17",
4343
"vite": "8.2.0",
44-
"watchpack": "2.5.2"
44+
"watchpack": "2.5.2",
45+
"xxhash-wasm": "1.1.0"
4546
},
4647
"optionalDependencies": {
4748
"lmdb": "3.5.6"

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { shutdownSassWorkerPool } from '../../tools/esbuild/stylesheets/sass-lan
1515
import { logMessages, withNoProgress, withSpinner } from '../../tools/esbuild/utils';
1616
import { ChangedFiles } from '../../tools/esbuild/watcher';
1717
import { shouldWatchRoot } from '../../utils/environment-options';
18+
import { initializeHash } from '../../utils/hash';
1819
import { NormalizedCachedOptions } from '../../utils/normalize-cache';
1920
import { toPosixPath } from '../../utils/path';
2021
import { NormalizedApplicationBuildOptions, NormalizedOutputOptions } from './options';
@@ -78,6 +79,8 @@ export async function* runEsBuildBuildAction(
7879
incrementalResults,
7980
} = options;
8081

82+
await initializeHash();
83+
8184
const withProgress: typeof withSpinner = progress ? withSpinner : withNoProgress;
8285

8386
// Initial build

packages/angular/build/src/builders/dev-server/vite/server.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
} from '../../../tools/vite/plugins';
2323
import { RolldownLoaderOption, getDepOptimizationConfig } from '../../../tools/vite/utils';
2424
import { loadProxyConfiguration } from '../../../utils';
25+
import { initializeHash } from '../../../utils/hash';
2526
import { type ApplicationBuilderInternalOptions, JavaScriptTransformer } from '../internal';
2627
import type { NormalizedDevServerOptions } from '../options';
2728
import { DevServerExternalResultMetadata, OutputAssetRecord, OutputFileRecord } from './utils';
@@ -147,6 +148,7 @@ export async function setupServer(
147148
indexHtmlTransformer?: (content: string) => Promise<string>,
148149
thirdPartySourcemaps = false,
149150
): Promise<Vite.InlineConfig> {
151+
await initializeHash();
150152
const { normalizePath } = (await import('vite' as string)) as typeof Vite;
151153

152154
// Path will not exist on disk and only used to provide separate path for Vite requests

packages/angular/build/src/builders/unit-test/test-discovery.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import { createHash } from 'node:crypto';
109
import { type PathLike, constants, promises as fs } from 'node:fs';
1110
import os from 'node:os';
1211
import { basename, dirname, extname, isAbsolute, join, relative } from 'node:path';
1312
import { glob, isDynamicPattern } from 'tinyglobby';
13+
import { calculateHash, initializeHash } from '../../utils/hash';
1414
import { toPosixPath } from '../../utils/path';
1515

1616
/**
@@ -41,6 +41,7 @@ export async function findTests(
4141
workspaceRoot: string,
4242
projectSourceRoot: string,
4343
): Promise<string[]> {
44+
await initializeHash();
4445
const resolvedTestFiles = new Set<string>();
4546
const dynamicPatterns: string[] = [];
4647

@@ -194,7 +195,7 @@ function truncateName(name: string, originalPath: string): string {
194195
return name;
195196
}
196197

197-
const hash = createHash('sha256').update(originalPath).digest('hex').substring(0, 8);
198+
const hash = calculateHash(originalPath).substring(0, 8);
198199
const availableLength = MAX_FILENAME_LENGTH - hash.length - 2; // 2 for '-' separators
199200
const prefixLength = Math.floor(availableLength / 2);
200201
const suffixLength = availableLength - prefixLength;

packages/angular/build/src/builders/unit-test/test-discovery_spec.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,14 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9+
import { initializeHash } from '../../utils/hash';
910
import { generateNameFromPath, getTestEntrypoints } from './test-discovery';
1011

1112
describe('getTestEntrypoints', () => {
13+
beforeAll(async () => {
14+
await initializeHash();
15+
});
16+
1217
const workspaceRoot = '/project';
1318
const projectSourceRoot = '/project/src';
1419
const options = { workspaceRoot, projectSourceRoot };
@@ -81,6 +86,10 @@ describe('getTestEntrypoints', () => {
8186
});
8287

8388
describe('generateNameFromPath', () => {
89+
beforeAll(async () => {
90+
await initializeHash();
91+
});
92+
8493
const roots = ['/project/src/', '/project/'];
8594

8695
it('should generate a dash-cased name from a simple path', () => {
@@ -127,7 +136,7 @@ describe('generateNameFromPath', () => {
127136

128137
expect(result.length).toBeLessThanOrEqual(128);
129138
expect(result).toBe(
130-
'a-very-long-path-that-definitely-exceeds-the-maximum-allowe-9cf40291-me-in-order-to-trigger-the-truncation-logic-in-the-function',
139+
'a-very-long-path-that-definitely-exceeds-the-maximum-allowe-4af8113d-me-in-order-to-trigger-the-truncation-logic-in-the-function',
131140
); // eslint-disable-line max-len
132141
});
133142

packages/angular/build/src/tools/angular/angular-host.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@
88

99
import type * as ng from '@angular/compiler-cli';
1010
import assert from 'node:assert';
11-
import { createHash } from 'node:crypto';
1211
import nodePath from 'node:path';
1312
import type ts from 'typescript';
13+
import { calculateHash } from '../../utils/hash';
1414

1515
export type AngularCompilerOptions = ng.CompilerOptions;
1616
export type AngularCompilerHost = ng.CompilerHost;
@@ -46,7 +46,7 @@ export function ensureSourceFileVersions(program: ts.Program): void {
4646

4747
for (const file of files) {
4848
if (file.version === undefined) {
49-
file.version = createHash('sha256').update(file.text).digest('hex');
49+
file.version = calculateHash(file.text);
5050
}
5151
}
5252

@@ -227,7 +227,7 @@ export function createAngularCompilerHost(
227227
// For external stylesheets, create a unique identifier and store the mapping
228228
let externalId = hostOptions.externalStylesheets.get(resolvedPath);
229229
if (externalId === undefined) {
230-
externalId = createHash('sha256').update(resolvedPath).digest('hex');
230+
externalId = calculateHash(resolvedPath);
231231
hostOptions.externalStylesheets.set(resolvedPath, externalId);
232232
}
233233

packages/angular/build/src/tools/esbuild/angular/compiler-plugin.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@ import type {
1818
PluginBuild,
1919
} from 'esbuild';
2020
import assert from 'node:assert';
21-
import { createHash } from 'node:crypto';
2221
import { readFile } from 'node:fs/promises';
2322
import * as path from 'node:path';
2423
import { maxWorkers, useTypeChecking } from '../../../utils/environment-options';
24+
import { calculateHash, initializeHash } from '../../../utils/hash';
2525
import { AngularHostOptions } from '../../angular/angular-host';
2626
import { AngularCompilation, DiagnosticModes, NoopCompilation } from '../../angular/compilation';
2727
import { type PersistentCacheStore, createPersistentCacheStore } from '../cache';
@@ -149,6 +149,7 @@ export function createCompilerPlugin(
149149

150150
// eslint-disable-next-line max-lines-per-function
151151
build.onStart(async () => {
152+
await initializeHash();
152153
angularCompilationContext.markAsInProgress();
153154

154155
const result: OnStartResult = {
@@ -205,11 +206,7 @@ export function createCompilerPlugin(
205206
// invalid the output and force a full page reload for HMR cases. The containing file and order
206207
// of the style within the containing file is used.
207208
pluginOptions.externalRuntimeStyles
208-
? createHash('sha256')
209-
.update(containingFile)
210-
.update((order ?? 0).toString())
211-
.update(className ?? '')
212-
.digest('hex')
209+
? calculateHash(`${containingFile}${order ?? 0}${className ?? ''}`)
213210
: undefined,
214211
);
215212
// Adjust result source for inline styles.

packages/angular/build/src/tools/esbuild/angular/component-stylesheets.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
*/
88

99
import assert from 'node:assert';
10-
import { createHash } from 'node:crypto';
1110
import path from 'node:path';
11+
import { createContentHash } from '../../../utils/hash';
1212
import { BundleContextResult, BundlerContext } from '../bundler-context';
1313
import { type BuildOutputFile, BuildOutputFileType } from '../bundler-files';
1414
import { MemoryCache } from '../cache';
@@ -103,11 +103,10 @@ export class ComponentStylesheetBundler {
103103
): Promise<ComponentStylesheetResult> {
104104
// Use a hash of the inline stylesheet content to ensure a consistent identifier. External stylesheets will resolve
105105
// to the actual stylesheet file path.
106-
// TODO: Consider xxhash instead for hashing
107-
const id = createHash('sha256')
108-
.update(data)
109-
.update(externalId ?? '')
110-
.digest('hex');
106+
const hasher = createContentHash();
107+
hasher.update(data);
108+
hasher.update(externalId ?? '');
109+
const id = hasher.digest();
111110
const entry = [language, id, filename].join(';');
112111

113112
const bundlerContext = await this.#inlineContexts.getOrCreate(entry, () => {

packages/angular/build/src/tools/esbuild/application-code-bundle.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@
88

99
import type { BuildOptions, Plugin } from 'esbuild';
1010
import assert from 'node:assert';
11-
import { createHash } from 'node:crypto';
1211
import { extname, relative } from 'node:path';
1312
import type { NormalizedApplicationBuildOptions } from '../../builders/application/options';
1413
import { Platform } from '../../builders/application/schema';
1514
import { allowMangle } from '../../utils/environment-options';
15+
import { calculateHash } from '../../utils/hash';
1616
import { toPosixPath } from '../../utils/path';
1717
import {
1818
SERVER_APP_ENGINE_MANIFEST_FILENAME,
@@ -566,7 +566,7 @@ function getEsBuildCommonOptions(options: NormalizedApplicationBuildOptions): Bu
566566
'',
567567
);
568568

569-
footer = { js: `/**i18n:${createHash('sha256').update(i18nHash).digest('hex')}*/` };
569+
footer = { js: `/**i18n:${calculateHash(i18nHash)}*/` };
570570
}
571571

572572
// Core conditions that are always included

0 commit comments

Comments
 (0)