From 08dcb6ebf2ad7c0b4b1f3fea2b5a57955d93bff2 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Tue, 20 Oct 2026 09:45:24 +0000 Subject: [PATCH 1/2] perf(@angular/build): avoid reading external component stylesheets twice - Skip calling `this.readFile(filename)` in `host.readResource(filename)` when `!hasTemplateExtension(filename)` so external component stylesheets (`styleUrl` / `styleUrls`) are not read from disk on the Angular compiler worker thread and serialized over the worker `MessagePort` only to be ignored by `transformStylesheet`. - Only short-circuit empty `data` in `host.transformResource(data, context)` for inline styles (`!context.resourceFile`) so external stylesheets still proceed to `transformStylesheet`. --- .../build/src/tools/angular/angular-host.ts | 10 +- .../src/tools/angular/angular-host_spec.ts | 98 +++++++++++++++++++ 2 files changed, 106 insertions(+), 2 deletions(-) create mode 100644 packages/angular/build/src/tools/angular/angular-host_spec.ts diff --git a/packages/angular/build/src/tools/angular/angular-host.ts b/packages/angular/build/src/tools/angular/angular-host.ts index f30df5bab64d..214666908ddf 100644 --- a/packages/angular/build/src/tools/angular/angular-host.ts +++ b/packages/angular/build/src/tools/angular/angular-host.ts @@ -182,6 +182,12 @@ export function createAngularCompilerHost( // The AOT compiler currently requires this hook to allow for a transformResource hook. // Once the AOT compiler allows only a transformResource hook, this can be reevaluated. host.readResource = async function (filename) { + // Only template resources need to be read by the compiler host. + // External stylesheets are read and bundled by the stylesheet bundler during `transformResource`. + if (!hasTemplateExtension(filename)) { + return ''; + } + return this.readFile(filename) ?? ''; }; @@ -197,8 +203,8 @@ export function createAngularCompilerHost( 'External runtime stylesheets should not be transformed: ' + context.resourceFile, ); - // No transformation required if the resource is empty - if (data.trim().length === 0) { + // No transformation required if an inline resource is empty + if (!context.resourceFile && data.trim().length === 0) { return { content: '' }; } diff --git a/packages/angular/build/src/tools/angular/angular-host_spec.ts b/packages/angular/build/src/tools/angular/angular-host_spec.ts new file mode 100644 index 000000000000..a905e0a853b4 --- /dev/null +++ b/packages/angular/build/src/tools/angular/angular-host_spec.ts @@ -0,0 +1,98 @@ +/** + * @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 ts from 'typescript'; +import { type AngularHostOptions, createAngularCompilerHost } from './angular-host'; + +describe('createAngularCompilerHost', () => { + function createHost(overrides?: Partial) { + const transformStylesheet = jasmine + .createSpy('transformStylesheet') + .and.resolveTo('.compiled {}'); + const processWebWorker = jasmine + .createSpy('processWebWorker') + .and.returnValue('worker.js'); + + const host = createAngularCompilerHost( + ts, + {}, + { + transformStylesheet, + processWebWorker, + ...overrides, + }, + undefined, + ); + + return { host, transformStylesheet }; + } + + describe('readResource', () => { + it('should not call readFile for .css and .scss stylesheet files', async () => { + const { host } = createHost(); + const readFileSpy = spyOn(host, 'readFile').and.returnValue('.btn { color: red; }'); + + expect(await host.readResource?.('/app/src/app.component.css')).toBe(''); + expect(await host.readResource?.('/app/src/app.component.scss')).toBe(''); + expect(readFileSpy).not.toHaveBeenCalled(); + }); + + it('should call readFile for .html and .svg template files', async () => { + const { host } = createHost(); + const readFileSpy = spyOn(host, 'readFile').and.callFake( + (fileName) => ``, + ); + + expect(await host.readResource?.('/app/src/app.component.html')).toBe( + '', + ); + expect(await host.readResource?.('/app/src/icon.component.svg')).toBe( + '', + ); + expect(readFileSpy).toHaveBeenCalledTimes(2); + }); + }); + + describe('transformResource', () => { + it('should invoke transformStylesheet when context.resourceFile is set even if data is empty', async () => { + const { host, transformStylesheet } = createHost(); + + const result = await host.transformResource?.('', { + type: 'style', + containingFile: '/app/src/app.component.ts', + resourceFile: '/app/src/app.component.scss', + order: 0, + className: 'AppComponent', + }); + + expect(result).toEqual({ content: '.compiled {}' }); + expect(transformStylesheet).toHaveBeenCalledOnceWith( + '', + '/app/src/app.component.ts', + '/app/src/app.component.scss', + 0, + 'AppComponent', + ); + }); + + it('should short-circuit empty inline styles without calling transformStylesheet', async () => { + const { host, transformStylesheet } = createHost(); + + const result = await host.transformResource?.(' ', { + type: 'style', + containingFile: '/app/src/app.component.ts', + resourceFile: null, + order: 0, + className: 'AppComponent', + }); + + expect(result).toEqual({ content: '' }); + expect(transformStylesheet).not.toHaveBeenCalled(); + }); + }); +}); From caf6b2a846662d1fecbb2f5fdfcb726e5b4380bb Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Tue, 20 Oct 2026 13:07:24 +0000 Subject: [PATCH 2/2] fixup! perf(@angular/build): avoid reading external component stylesheets twice --- packages/angular/build/src/tools/angular/angular-host_spec.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/angular/build/src/tools/angular/angular-host_spec.ts b/packages/angular/build/src/tools/angular/angular-host_spec.ts index a905e0a853b4..ab2b6151aed0 100644 --- a/packages/angular/build/src/tools/angular/angular-host_spec.ts +++ b/packages/angular/build/src/tools/angular/angular-host_spec.ts @@ -10,7 +10,7 @@ import ts from 'typescript'; import { type AngularHostOptions, createAngularCompilerHost } from './angular-host'; describe('createAngularCompilerHost', () => { - function createHost(overrides?: Partial) { + function createHost() { const transformStylesheet = jasmine .createSpy('transformStylesheet') .and.resolveTo('.compiled {}'); @@ -24,7 +24,6 @@ describe('createAngularCompilerHost', () => { { transformStylesheet, processWebWorker, - ...overrides, }, undefined, );