Skip to content

perf(@angular/build): consolidate component stylesheet bundling with shared load result cache - #33906

Open
alan-agius4 wants to merge 1 commit into
angular:mainfrom
alan-agius4:perf-build-component-stylesheet-context-pooling
Open

perf(@angular/build): consolidate component stylesheet bundling with shared load result cache#33906
alan-agius4 wants to merge 1 commit into
angular:mainfrom
alan-agius4:perf-build-component-stylesheet-context-pooling

Conversation

@alan-agius4

@alan-agius4 alan-agius4 commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Replaces per-stylesheet esbuild.context proliferation in ComponentStylesheetBundler with ephemeral single-shot esbuild.build() invocations and a shared, persistent MemoryLoadResultCache.

Key Improvements

  1. Eliminates Go process IPC context proliferation (500 active goroutines/contexts -> 0 lingering contexts).
  2. Cross-component Sass partial & preprocessor cache sharing via unified MemoryLoadResultCache.
  3. In-flight compilation deduplication coalescing concurrent requests.
  4. Optimized set intersection invalidation (O(min(|A|, |B|))) accelerating large-scale file invalidations by up to 70x.

Comprehensive Benchmarks

1. Cold Compilation Throughput & Latency (Mean Duration)

Workload Baseline Consolidated Speedup Latency Reduction
100 Components (Cold) 114.96 ms 98.74 ms 1.16x -14.1%
300 Components (Cold) 358.37 ms 315.68 ms 1.14x -11.9%
500 Components (Cold) 609.68 ms 530.87 ms 1.15x -12.9%

2. Memory Footprint (500 Components)

Metric Baseline (Contexts) Consolidated (Shared Cache) Delta / Improvement
Persistent Go IPC Contexts 500 active 0 contexts -100.0% (Eliminated)
V8 Heap Used Delta +10.22 MB +5.79 MB -43.3% Heap Reduction
Resident Set Size (RSS) Delta +27.60 MB +16.30 MB -41.0% RSS Reduction

3. Watch Mode Rebuild & Invalidation Latencies

Scenario Baseline Consolidated Speedup Factor
Leaf Component Stylesheet Edit 7.96 ms 6.75 ms 1.18x Faster
Shared Sass Partial Edit 227.18 ms 206.63 ms 1.10x Faster
Branch Switch (500 modified / 500 components) 2.27 ms/op 0.14 ms/op 16.07x Faster
Large Switch (1,000 modified / 1,000 components) 28.38 ms/op 0.40 ms/op 70.65x Faster
No-Op / Touched File Rebuild 0.47 ms 0.29 ms 1.62x Faster

@angular-robot angular-robot Bot added area: performance Issues related to performance area: @angular/build labels Aug 24, 2026
@alan-agius4

Copy link
Copy Markdown
Collaborator Author

/gemini review

@angular angular deleted a comment from gemini-code-assist Bot Aug 24, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors ComponentStylesheetBundler to run esbuild compilations directly instead of using BundlerContext, introducing custom caching and promise-based deduplication for both file and inline stylesheets. The review feedback highlights a potential bug where externalId remains a boolean if the cached identifier is missing, a performance optimization in the cache invalidation logic using set intersection, and a missing guard in extractResult that could lead to a TypeError if result.metafile is undefined.

Comment thread packages/angular/build/src/tools/esbuild/angular/component-stylesheets.ts Outdated
@alan-agius4
alan-agius4 force-pushed the perf-build-component-stylesheet-context-pooling branch from a088bda to 4cb873f Compare August 24, 2026 14:58
@alan-agius4 alan-agius4 added the target: patch This PR is targeted for the next patch release label Aug 24, 2026
@alan-agius4
alan-agius4 requested a review from clydin August 24, 2026 15:10
@alan-agius4 alan-agius4 added the action: review The PR is still awaiting reviews from at least one requested reviewer label Aug 24, 2026
@alan-agius4
alan-agius4 marked this pull request as ready for review August 24, 2026 15:10

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors ComponentStylesheetBundler to perform direct esbuild compilation using build() instead of wrapping it in BundlerContext. It introduces in-memory caching for file and inline results, along with tracking active build promises to support incremental watch mode. The review feedback highlights critical race conditions and stale cache write issues in both file-based and inline stylesheet bundling when files are modified while a build promise is in-flight. It is recommended to clear pending promises on invalidation, ensure only active promises write to the cache, and extract a helper function to resolve absolute paths to reduce code duplication in #collectWatchFiles.

@alan-agius4 alan-agius4 added target: minor This PR is targeted for the next minor release and removed target: patch This PR is targeted for the next patch release labels Aug 24, 2026
…shared load result cache

Replaces per-stylesheet `esbuild.context` proliferation in `ComponentStylesheetBundler` with ephemeral single-shot `esbuild.build()` invocations and a shared, persistent `MemoryLoadResultCache`.

### Key Improvements
1. Eliminates Go process IPC context proliferation (500 active goroutines/contexts -> 0 lingering contexts).
2. Cross-component Sass partial & preprocessor cache sharing via unified `MemoryLoadResultCache`.
3. In-flight compilation deduplication coalescing concurrent requests.
4. Optimized set intersection invalidation (O(min(|A|, |B|))) accelerating large-scale file invalidations by up to 70x.

### Comprehensive Benchmarks

#### 1. Cold Compilation Throughput & Latency (Mean Duration)

| Workload | Baseline | Consolidated | Speedup | Latency Reduction |
| :--- | :--- | :--- | :--- | :--- |
| 100 Components (Cold) | 114.96 ms | 98.74 ms | 1.16x | -14.1% |
| 300 Components (Cold) | 358.37 ms | 315.68 ms | 1.14x | -11.9% |
| 500 Components (Cold) | 609.68 ms | 530.87 ms | 1.15x | -12.9% |

#### 2. Memory Footprint (500 Components)

| Metric | Baseline (Contexts) | Consolidated (Shared Cache) | Delta / Improvement |
| :--- | :--- | :--- | :--- |
| Persistent Go IPC Contexts | 500 active | 0 contexts | -100.0% (Eliminated) |
| V8 Heap Used Delta | +10.22 MB | +5.79 MB | -43.3% Heap Reduction |
| Resident Set Size (RSS) Delta | +27.60 MB | +16.30 MB | -41.0% RSS Reduction |

#### 3. Watch Mode Rebuild & Invalidation Latencies

| Scenario | Baseline | Consolidated | Speedup Factor |
| :--- | :--- | :--- | :--- |
| Leaf Component Stylesheet Edit | 7.96 ms | 6.75 ms | 1.18x Faster |
| Shared Sass Partial Edit | 227.18 ms | 206.63 ms | 1.10x Faster |
| Branch Switch (500 modified / 500 components) | 2.27 ms/op | 0.14 ms/op | 16.07x Faster |
| Large Switch (1,000 modified / 1,000 components) | 28.38 ms/op | 0.40 ms/op | 70.65x Faster |
| No-Op / Touched File Rebuild | 0.47 ms | 0.29 ms | 1.62x Faster |
@alan-agius4
alan-agius4 force-pushed the perf-build-component-stylesheet-context-pooling branch from 4cb873f to 57de14f Compare August 24, 2026 15:28
Comment on lines -57 to -70
const bundlerContext = await this.#fileContexts.getOrCreate(entry, () => {
return new BundlerContext(this.options.workspaceRoot, this.incremental, (loadCache) => {
const buildOptions = createStylesheetBundleOptions(this.options, loadCache);
if (externalId) {
assert(
typeof externalId === 'string',
'Initial external component stylesheets must have a string identifier',
);

buildOptions.entryPoints = { [externalId]: entry };
buildOptions.entryNames = '[name]';
delete buildOptions.publicPath;
} else {
buildOptions.entryPoints = [entry];

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With #33914, you should be able to do something similar to the following now:

const bundlerContext = await this.#fileContexts.getOrCreate(entry, () => {
  return new BundlerContext(
    this.options.workspaceRoot,
    this.incremental,
    (loadCache) => createStylesheetBundleOptions(this.options, loadCache, ...),
    /* useContext */ false,        // Enables direct esbuild.build() mode
    /* initialFilter */ undefined, // Default filter
    this.#loadCache,               // Shared LoadResultCache instance
  );
});

Comment on lines +481 to +497
const hasIntersection = (setA: Set<string>, setB: Set<string>): boolean => {
if (setA.size < setB.size) {
for (const value of setA) {
if (setB.has(value)) {
return true;
}
}
} else {
for (const value of setB) {
if (setA.has(value)) {
return true;
}
}
}

return false;
};

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we move this logic into the BundlerContext's invalidate directly so that all usages could benefit? Probably need to update the signature to invalidate(files: Iterable<string> | ReadonlySet<string>): boolean.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

action: review The PR is still awaiting reviews from at least one requested reviewer area: @angular/build area: performance Issues related to performance target: minor This PR is targeted for the next minor release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants