Skip to content

Commit 6ee559c

Browse files
committed
perf(@angular/build): bypass worker dispatch for files without transform candidates
Files that require neither Angular linking nor advanced optimizations previously underwent full worker thread IPC dispatch, worker deserialization, and AST processing. By evaluating a fast candidate check on the main thread directly on raw data buffers, files that cannot be modified by advanced optimizations immediately return the original data buffer without worker dispatch or AST parsing.
1 parent 88e3fc0 commit 6ee559c

2 files changed

Lines changed: 217 additions & 5 deletions

File tree

packages/angular/build/src/tools/esbuild/javascript-transformer.ts

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,76 @@ import { Cache } from './cache';
1616
const LINKER_DECLARATION_PREFIX = 'ɵɵngDeclare';
1717
const LINKER_DECLARATION_PREFIX_BYTES = Buffer.from(LINKER_DECLARATION_PREFIX, 'utf-8');
1818

19+
const ADVANCED_OPTIMIZATION_TOKENS = [
20+
'ɵ',
21+
'InjectionToken',
22+
'INJECTOR_KEY',
23+
'ctorParameters',
24+
'decorators',
25+
'propDecorators',
26+
] as const;
27+
28+
const ADVANCED_OPTIMIZATION_TOKEN_BYTES = ADVANCED_OPTIMIZATION_TOKENS.map((token) =>
29+
Buffer.from(token, 'utf-8'),
30+
);
31+
32+
const DECORATOR_TOKENS = ['__decorate', '__esDecorate'] as const;
33+
const DECORATOR_TOKEN_BYTES = DECORATOR_TOKENS.map((token) => Buffer.from(token, 'utf-8'));
34+
35+
const ADVANCED_OPTIMIZATION_REGEX = new RegExp(ADVANCED_OPTIMIZATION_TOKENS.join('|'));
36+
const ADVANCED_OPTIMIZATION_WITH_DECORATORS_REGEX = new RegExp(
37+
[...ADVANCED_OPTIMIZATION_TOKENS, ...DECORATOR_TOKENS].join('|'),
38+
);
39+
40+
/**
41+
* Determines whether JavaScript code contains potential candidate constructs for advanced optimizations.
42+
* When false, advanced optimizations can be bypassed without worker dispatch or AST parsing.
43+
*
44+
* @param filename The full path to the file.
45+
* @param data The data (string or Buffer) of the file.
46+
* @param sideEffects If false, indicates the file is considered side-effect free.
47+
* @returns True if the code may contain constructs that advanced optimizations can mutate.
48+
*/
49+
function hasAdvancedOptimizationCandidates(
50+
filename: string,
51+
data: string | Uint8Array,
52+
sideEffects?: boolean,
53+
): boolean {
54+
// Side-effect-free @angular/ packages undergo top-level pure function annotations
55+
if (sideEffects === false && /[\\/]node_modules[\\/]@angular[\\/]/.test(filename)) {
56+
return true;
57+
}
58+
59+
if (typeof data === 'string') {
60+
const regex =
61+
sideEffects === false
62+
? ADVANCED_OPTIMIZATION_WITH_DECORATORS_REGEX
63+
: ADVANCED_OPTIMIZATION_REGEX;
64+
65+
return regex.test(data);
66+
}
67+
68+
const dataBuffer = Buffer.isBuffer(data)
69+
? data
70+
: Buffer.from(data.buffer, data.byteOffset, data.byteLength);
71+
72+
for (const tokenBytes of ADVANCED_OPTIMIZATION_TOKEN_BYTES) {
73+
if (dataBuffer.includes(tokenBytes)) {
74+
return true;
75+
}
76+
}
77+
78+
if (sideEffects === false) {
79+
for (const tokenBytes of DECORATOR_TOKEN_BYTES) {
80+
if (dataBuffer.includes(tokenBytes)) {
81+
return true;
82+
}
83+
}
84+
}
85+
86+
return false;
87+
}
88+
1989
/**
2090
* Determines whether JavaScript code requires Angular linker processing.
2191
*
@@ -220,10 +290,13 @@ export class JavaScriptTransformer {
220290
instrumentForCoverage?: boolean,
221291
): Promise<Uint8Array> {
222292
const shouldLink = !skipLinker && requiresLinking(filename, data);
293+
const shouldOptimize =
294+
this.#commonOptions.advancedOptimizations &&
295+
hasAdvancedOptimizationCandidates(filename, data, sideEffects);
223296

224297
// Perform a quick test to determine if the data needs any transformations.
225298
// This allows directly returning the data without the worker communication overhead.
226-
if (!shouldLink && !this.#commonOptions.advancedOptimizations && !instrumentForCoverage) {
299+
if (!shouldLink && !shouldOptimize && !instrumentForCoverage) {
227300
const keepSourcemap =
228301
this.#commonOptions.sourcemap &&
229302
(!!this.#commonOptions.thirdPartySourcemaps || !/[\\/]node_modules[\\/]/.test(filename));

packages/angular/build/src/tools/esbuild/javascript-transformer_spec.ts

Lines changed: 143 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,12 @@ describe('JavaScriptTransformer sourcemaps', () => {
3838
const inputMap = {
3939
version: 3,
4040
sources: ['src/app.ts'],
41-
sourcesContent: ['const x = new SomeClass();'],
41+
sourcesContent: ['export class MyClass { static ɵprov = 42; }'],
4242
mappings: 'AAAA',
4343
names: [],
4444
};
4545
const base64Map = Buffer.from(JSON.stringify(inputMap)).toString('base64');
46-
const input = `var x = new SomeClass();\n//# sourceMappingURL=data:application/json;base64,${base64Map}`;
46+
const input = `export class MyClass { static ɵprov = 42; }\n//# sourceMappingURL=data:application/json;base64,${base64Map}`;
4747

4848
const result = await transformer.transformData('src/app.js', input, true);
4949
const text = Buffer.from(result).toString('utf-8');
@@ -157,7 +157,7 @@ describe('JavaScriptTransformer sourcemaps', () => {
157157
1,
158158
);
159159

160-
const input = 'var x = new SomeClass();';
160+
const input = 'export class MyClass { static ɵprov = 42; }';
161161
const result = await transformer.transformData('src/app.js', input, true);
162162
const text = Buffer.from(result).toString('utf-8');
163163
const map = extractSourcemap(text);
@@ -249,7 +249,7 @@ describe('JavaScriptTransformer sourcemaps', () => {
249249
1,
250250
);
251251

252-
const inputBuffer = Buffer.from('var x = new SomeClass();', 'utf-8');
252+
const inputBuffer = Buffer.from('export class MyClass { static ɵprov = 42; }', 'utf-8');
253253
const result = await transformer.transformData('src/app.js', inputBuffer, true);
254254
const text = Buffer.from(result).toString('utf-8');
255255
const map = extractSourcemap(text);
@@ -374,4 +374,143 @@ describe('JavaScriptTransformer sourcemaps', () => {
374374

375375
expect(text).not.toContain('i0.ɵɵngDeclareDirective');
376376
});
377+
378+
describe('advanced optimizations fast-path pre-filter', () => {
379+
it('should bypass worker and return input buffer directly when no candidate tokens are present', async () => {
380+
transformer = new JavaScriptTransformer(
381+
{
382+
sourcemap: false,
383+
advancedOptimizations: true,
384+
},
385+
1,
386+
);
387+
388+
const inputBuffer = Buffer.from(
389+
'function add(a, b) { return a + b; }\nconst result = add(1, 2);',
390+
'utf-8',
391+
);
392+
const result = await transformer.transformData('src/math.js', inputBuffer, true);
393+
394+
expect(result).toBe(inputBuffer);
395+
});
396+
397+
it('should bypass worker for standard classes without static properties', async () => {
398+
transformer = new JavaScriptTransformer(
399+
{
400+
sourcemap: false,
401+
advancedOptimizations: true,
402+
},
403+
1,
404+
);
405+
406+
const inputBuffer = Buffer.from(
407+
`export class UserService {
408+
constructor(http) { this.http = http; }
409+
getUser(id) { return this.http.get('/users/' + id); }
410+
}`,
411+
'utf-8',
412+
);
413+
const result = await transformer.transformData('src/user.service.js', inputBuffer, true);
414+
415+
expect(result).toBe(inputBuffer);
416+
});
417+
418+
it('should bypass worker for default exports without static properties or Angular metadata', async () => {
419+
transformer = new JavaScriptTransformer(
420+
{
421+
sourcemap: false,
422+
advancedOptimizations: true,
423+
},
424+
1,
425+
);
426+
427+
const inputBuffer = Buffer.from(
428+
`export default class UserService {
429+
constructor(http) { this.http = http; }
430+
getUser(id) { return this.http.get('/users/' + id); }
431+
}`,
432+
'utf-8',
433+
);
434+
const result = await transformer.transformData('src/user.service.js', inputBuffer, true);
435+
436+
expect(result).toBe(inputBuffer);
437+
});
438+
439+
it('should bypass worker for classes with static members when no Angular metadata is present', async () => {
440+
transformer = new JavaScriptTransformer(
441+
{
442+
sourcemap: false,
443+
advancedOptimizations: true,
444+
},
445+
1,
446+
);
447+
448+
const inputBuffer = Buffer.from('export class MyComponent { static prop = 42; }', 'utf-8');
449+
const result = await transformer.transformData('src/component.js', inputBuffer, true);
450+
451+
expect(result).toBe(inputBuffer);
452+
});
453+
454+
it('should dispatch to worker when Angular tokens are present', async () => {
455+
transformer = new JavaScriptTransformer(
456+
{
457+
sourcemap: false,
458+
advancedOptimizations: true,
459+
},
460+
1,
461+
);
462+
463+
const input = 'export class MyService { static ɵprov = true; }';
464+
const result = await transformer.transformData('src/service.js', input, true);
465+
const text = Buffer.from(result).toString('utf-8');
466+
467+
expect(text).toContain('let MyService = /*#__PURE__*/ (() => {');
468+
});
469+
470+
it('should dispatch to worker when decorator tokens are present and sideEffects is false', async () => {
471+
transformer = new JavaScriptTransformer(
472+
{
473+
sourcemap: false,
474+
advancedOptimizations: true,
475+
},
476+
1,
477+
);
478+
479+
const inputBuffer = Buffer.from('const MyClass = __decorate([], class {});', 'utf-8');
480+
const result = await transformer.transformData('src/class.js', inputBuffer, true, false);
481+
482+
expect(result).not.toBe(inputBuffer);
483+
});
484+
485+
it('should bypass worker and return converted buffer when no candidate tokens are present in string input', async () => {
486+
transformer = new JavaScriptTransformer(
487+
{
488+
sourcemap: false,
489+
advancedOptimizations: true,
490+
},
491+
1,
492+
);
493+
494+
const inputString = 'function multiply(a, b) { return a * b; }';
495+
const result = await transformer.transformData('src/math.js', inputString, true);
496+
497+
expect(Buffer.from(result).toString('utf-8')).toBe(inputString);
498+
});
499+
500+
it('should dispatch to worker when candidate tokens are present in string input', async () => {
501+
transformer = new JavaScriptTransformer(
502+
{
503+
sourcemap: false,
504+
advancedOptimizations: true,
505+
},
506+
1,
507+
);
508+
509+
const inputString = 'export class MyService { static ɵprov = true; }';
510+
const result = await transformer.transformData('src/service.js', inputString, true);
511+
const text = Buffer.from(result).toString('utf-8');
512+
513+
expect(text).toContain('let MyService = /*#__PURE__*/ (() => {');
514+
});
515+
});
377516
});

0 commit comments

Comments
 (0)