From 51a4e04a747129a0f67cbfc34fc027a33856be94 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Tue, 20 Oct 2026 09:18:03 +0000 Subject: [PATCH 1/2] perf(@angular/build): avoid web worker channel overhead in library parallel compilation Skip creating and transferring `webWorkerChannel` (`MessageChannel`) and `webWorkerSignal` (`SharedArrayBuffer`) in `ParallelCompilation.initialize()` when `buildType === 'library'`, as library builds do not bundle web workers via `processWebWorker` and previously left ports open until the next build if `emitAffectedFiles()` was skipped due to stylesheet errors. Also replace `crypto.randomUUID()` per component stylesheet request in `parallel-worker.ts` with an incrementing numeric request counter. --- .../compilation/parallel-compilation.ts | 47 +++++++++------ .../compilation/parallel-compilation_spec.ts | 59 +++++++++++++++++++ .../angular/compilation/parallel-worker.ts | 14 +++-- 3 files changed, 96 insertions(+), 24 deletions(-) create mode 100644 packages/angular/build/src/tools/angular/compilation/parallel-compilation_spec.ts diff --git a/packages/angular/build/src/tools/angular/compilation/parallel-compilation.ts b/packages/angular/build/src/tools/angular/compilation/parallel-compilation.ts index 6e4debf62501..883b880dac01 100644 --- a/packages/angular/build/src/tools/angular/compilation/parallel-compilation.ts +++ b/packages/angular/build/src/tools/angular/compilation/parallel-compilation.ts @@ -8,7 +8,7 @@ import type { PartialMessage } from 'esbuild'; import { createRequire } from 'node:module'; -import { MessageChannel } from 'node:worker_threads'; +import { MessageChannel, type MessagePort } from 'node:worker_threads'; import { mergeCumulativeDurations } from '../../../utils/profiling'; import { WorkerPool } from '../../../utils/worker-pool'; import type { AngularHostOptions } from '../angular-host'; @@ -67,23 +67,30 @@ export class ParallelCompilation extends AngularCompilation { ); this.#webWorkerChannel?.port1.close(); + this.#webWorkerChannel = undefined; - // The web worker processing is a synchronous operation and uses shared memory combined with - // the Atomics API to block execution here until a response is received. - const webWorkerChannel = new MessageChannel(); - this.#webWorkerChannel = webWorkerChannel; - const webWorkerSignal = new Int32Array(new SharedArrayBuffer(4)); - webWorkerChannel.port1.on('message', ({ workerFile, containingFile }) => { - try { - const workerCodeFile = hostOptions.processWebWorker(workerFile, containingFile); - webWorkerChannel.port1.postMessage({ workerCodeFile }); - } catch (error) { - webWorkerChannel.port1.postMessage({ error }); - } finally { - Atomics.store(webWorkerSignal, 0, 1); - Atomics.notify(webWorkerSignal, 0); - } - }); + let webWorkerPort: MessagePort | undefined; + let webWorkerSignal: Int32Array | undefined; + if (buildType !== 'library') { + // The web worker processing is a synchronous operation and uses shared memory combined with + // the Atomics API to block execution here until a response is received. + const webWorkerChannel = new MessageChannel(); + this.#webWorkerChannel = webWorkerChannel; + const signal = new Int32Array(new SharedArrayBuffer(4)); + webWorkerPort = webWorkerChannel.port2; + webWorkerSignal = signal; + webWorkerChannel.port1.on('message', ({ workerFile, containingFile }) => { + try { + const workerCodeFile = hostOptions.processWebWorker(workerFile, containingFile); + webWorkerChannel.port1.postMessage({ workerCodeFile }); + } catch (error) { + webWorkerChannel.port1.postMessage({ error }); + } finally { + Atomics.store(signal, 0, 1); + Atomics.notify(signal, 0); + } + }); + } let success = false; try { @@ -97,12 +104,14 @@ export class ParallelCompilation extends AngularCompilation { compilerOptionOverrides, buildType, stylesheetPort: stylesheetChannel.port2, - webWorkerPort: webWorkerChannel.port2, + webWorkerPort, webWorkerSignal, }, { name: 'initialize', - transferList: [stylesheetChannel.port2, webWorkerChannel.port2], + transferList: webWorkerPort + ? [stylesheetChannel.port2, webWorkerPort] + : [stylesheetChannel.port2], }, ); success = true; diff --git a/packages/angular/build/src/tools/angular/compilation/parallel-compilation_spec.ts b/packages/angular/build/src/tools/angular/compilation/parallel-compilation_spec.ts new file mode 100644 index 000000000000..7157251e1ab9 --- /dev/null +++ b/packages/angular/build/src/tools/angular/compilation/parallel-compilation_spec.ts @@ -0,0 +1,59 @@ +/** + * @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.dev/license + */ + +import { MessageChannel } from 'node:worker_threads'; +import { AotCompilation } from './aot-compilation'; +import { initialize } from './parallel-worker'; + +describe('parallel-worker', () => { + it('uses incrementing numeric request IDs for transformStylesheet and handles omitted webWorkerPort in library builds', async () => { + const receivedRequestIds: unknown[] = []; + let workerResult: string | undefined; + + spyOn(AotCompilation.prototype, 'initialize').and.callFake(async (_tsconfig, hostOptions) => { + const [style1, style2] = await Promise.all([ + hostOptions.transformStylesheet('body {}', '/src/a.ts', '/src/a.css', 0, 'CompA'), + hostOptions.transformStylesheet('div {}', '/src/b.ts', '/src/b.css', 1, 'CompB'), + ]); + expect(style1).toBe('transformed:body {}'); + expect(style2).toBe('transformed:div {}'); + + workerResult = hostOptions.processWebWorker('./my.worker', '/src/a.ts'); + + return { + compilerOptions: {}, + referencedFiles: ['/src/a.ts'], + }; + }); + + const stylesheetChannel = new MessageChannel(); + stylesheetChannel.port1.on('message', ({ requestId, data }) => { + receivedRequestIds.push(requestId); + stylesheetChannel.port1.postMessage({ + requestId, + value: `transformed:${data}`, + }); + }); + + try { + const result = await initialize({ + jit: false, + browserOnlyBuild: false, + tsconfig: '/src/tsconfig.lib.json', + buildType: 'library', + stylesheetPort: stylesheetChannel.port2, + }); + + expect(receivedRequestIds).toEqual([1, 2]); + expect(workerResult).toBe('./my.worker'); + expect(result.referencedFiles).toEqual(['/src/a.ts']); + } finally { + stylesheetChannel.port1.close(); + } + }); +}); diff --git a/packages/angular/build/src/tools/angular/compilation/parallel-worker.ts b/packages/angular/build/src/tools/angular/compilation/parallel-worker.ts index 475592bd478b..dde3df4c0c20 100644 --- a/packages/angular/build/src/tools/angular/compilation/parallel-worker.ts +++ b/packages/angular/build/src/tools/angular/compilation/parallel-worker.ts @@ -8,7 +8,6 @@ import type { PartialMessage } from 'esbuild'; import assert from 'node:assert'; -import { randomUUID } from 'node:crypto'; import { type MessagePort, receiveMessageOnPort } from 'node:worker_threads'; import { initializeHash } from '../../../utils/hash'; import { getAndClearCumulativeDurations } from '../../../utils/profiling'; @@ -29,8 +28,8 @@ export interface InitRequest { compilerOptionOverrides?: CompilerOptionOverrides; buildType?: 'application' | 'library'; stylesheetPort: MessagePort; - webWorkerPort: MessagePort; - webWorkerSignal: Int32Array; + webWorkerPort?: MessagePort; + webWorkerSignal?: Int32Array; } let compilation: AngularCompilation | undefined; @@ -52,8 +51,9 @@ export async function initialize(request: InitRequest): Promise void, (reason: Error) => void] >(); request.stylesheetPort.on('message', ({ requestId, value, error }) => { @@ -81,7 +81,7 @@ export async function initialize(request: InitRequest): Promise((resolve, reject) => stylesheetRequests.set(requestId, [resolve, reject]), ); @@ -98,6 +98,10 @@ export async function initialize(request: InitRequest): Promise Date: Tue, 20 Oct 2026 13:04:21 +0000 Subject: [PATCH 2/2] fixup! perf(@angular/build): avoid web worker channel overhead in library parallel compilation --- .../tools/angular/compilation/parallel-compilation.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/angular/build/src/tools/angular/compilation/parallel-compilation.ts b/packages/angular/build/src/tools/angular/compilation/parallel-compilation.ts index 883b880dac01..60945a758081 100644 --- a/packages/angular/build/src/tools/angular/compilation/parallel-compilation.ts +++ b/packages/angular/build/src/tools/angular/compilation/parallel-compilation.ts @@ -8,7 +8,7 @@ import type { PartialMessage } from 'esbuild'; import { createRequire } from 'node:module'; -import { MessageChannel, type MessagePort } from 'node:worker_threads'; +import { MessageChannel } from 'node:worker_threads'; import { mergeCumulativeDurations } from '../../../utils/profiling'; import { WorkerPool } from '../../../utils/worker-pool'; import type { AngularHostOptions } from '../angular-host'; @@ -69,7 +69,6 @@ export class ParallelCompilation extends AngularCompilation { this.#webWorkerChannel?.port1.close(); this.#webWorkerChannel = undefined; - let webWorkerPort: MessagePort | undefined; let webWorkerSignal: Int32Array | undefined; if (buildType !== 'library') { // The web worker processing is a synchronous operation and uses shared memory combined with @@ -77,7 +76,6 @@ export class ParallelCompilation extends AngularCompilation { const webWorkerChannel = new MessageChannel(); this.#webWorkerChannel = webWorkerChannel; const signal = new Int32Array(new SharedArrayBuffer(4)); - webWorkerPort = webWorkerChannel.port2; webWorkerSignal = signal; webWorkerChannel.port1.on('message', ({ workerFile, containingFile }) => { try { @@ -104,13 +102,13 @@ export class ParallelCompilation extends AngularCompilation { compilerOptionOverrides, buildType, stylesheetPort: stylesheetChannel.port2, - webWorkerPort, + webWorkerPort: this.#webWorkerChannel?.port2, webWorkerSignal, }, { name: 'initialize', - transferList: webWorkerPort - ? [stylesheetChannel.port2, webWorkerPort] + transferList: this.#webWorkerChannel + ? [stylesheetChannel.port2, this.#webWorkerChannel.port2] : [stylesheetChannel.port2], }, );