Skip to content

Benchmark @angular/build:library against ng-packagr and incremental builds - #1

Merged
ChristopherPHolder merged 16 commits into
mainfrom
oxc-library-builder
Oct 8, 2026
Merged

ChristopherPHolder merged 16 commits into
mainfrom
oxc-library-builder

Conversation

@ChristopherPHolder

Copy link
Copy Markdown
Collaborator

Switches the workspace to the new Rolldown/Oxc-based @angular/build:library builder (Angular CLI 22.3.0-next.1) and turns the repo into a set of benchmarks comparing it with ng-packagr, and incremental builds with building from source, in build time and memory.

What's in here

  • Builder switch: every lib's build target uses @angular/build:library (entry points from package.json exports). ng-packagr stays available as a build-ng-packagr target writing to dist/ng-packagr. Apps now default to buildLibsFromSource: false, since dependsOn: ["^build"] builds the libs anyway.
  • New demos (tools/generate-entry-point-libs.mjs): 6 apps with 36 feature libs + 3 shared UI libs, each split into 51 secondary entry points (17,628 components), once with SCSS (entry-points) and once with plain CSS (entry-points-css).
  • Benchmark (tools/benchmark.mjs): cold / one app lib changed / one shared lib changed / cache hit, for from source vs incremental with each builder, recording wall time, peak RSS and lib vs app task time. The CI workflow runs one job per demo and mode and merges the results.
  • Experiments: tools/memory-experiments.mjs (default heap, heap limits, Angular thread settings, Nx parallelism) and tools/builder-factors.mjs (entry points × SCSS/CSS on one lib).
  • README: methodology, local and CI results, and when each approach is better or worse.

Headline results

  • The new builder is 1.3× faster than ng-packagr on single-entry-point libs and 2.6–3.6× faster on libs with many secondary entry points and SCSS. ng-packagr restarts its Sass workers for every entry point, so SCSS barely affects the new builder but doubles ng-packagr's lib time.
  • On CI a cold build of the SCSS multi-app demo takes 18.4 min with ng-packagr vs 6.8 min with the new builder (5.5 min from source). When one app lib changes, incremental builds with the new builder take about half as long as building from source.
  • app0 runs out of memory when built from source with Node's default heap, while both incremental builds succeed. NG_BUILD_PARALLEL_TS=0 / NG_BUILD_MAX_WORKERS=1 don't change memory meaningfully.

Notes

  • @angular/build 22.2+ breaks the Analog Vitest plugin, so Analog and the apps' unit tests are removed.
  • Dependencies need npm ci --legacy-peer-deps because of the CLI prerelease.
  • Upstream sources used for the analysis are cloned into the git-ignored .repos/ folder.

Upgrade the Angular CLI packages to 22.3.0-next.1, which ships the new
Oxc/Rolldown-based @angular/build:library builder, and use it for the
`build` target of every app0 lib. Entry points now come from the
package.json `exports` field.

Keep ng-packagr around as a `build-ng-packagr` target (plus a matching
target on app0) writing to dist/ng-packagr, so both builders can be
benchmarked side by side.

The shell lib's production tsconfig points its lib imports at dist,
which @nx/angular:package used to do automatically. Without it the
shell type-checked every other lib from source (37s instead of 1s).

@angular/build 22.2+ no longer exposes SourceFileCache as a Map, which
breaks the Analog Vitest plugin, so the Analog packages and the app's
Vitest setup are removed.
tools/generate-entry-point-libs.mjs generates 6 apps, each with 6
feature libs, plus 3 shared UI libs that every app uses. Every lib is
split into 51 secondary entry points with 8 components each, and every
component has an inline SCSS stylesheet. Feature libs consume the
shared libs from dist in their production build.

Each lib has both a `build` (@angular/build:library) and a
`build-ng-packagr` (@nx/angular:package) target, so the two builders
can be compared on the same code.

The benchmark now runs per demo (app0 and entry-points), builds all of
a demo's apps with run-many, adds a "shared lib changed" scenario,
clears ng-packagr's own cache for cold runs and reports lib and app
task time separately. The Benchmark workflow takes the demos to run
and allows up to 6 hours.
Every mode runs with the same --parallel value (Nx's default of 3
unless set). The Benchmark workflow defaults to 2, because building
three entry-points apps from source at once peaks around 17 GB, more
than the 16 GB GitHub runner has.
--styles=css writes the component stylesheets as plain CSS instead of
SCSS, and --layout=single puts every folder of a lib behind a single
primary entry point instead of one secondary entry point each. Together
they make it possible to compare the builders on otherwise identical
code. --demo=<name> generates a variant next to the default demo.

The defaults now match the committed demo, and its output is unchanged.
The Benchmark workflow now fans out into a job per demo and mode, so
the slow combinations run in parallel on separate runners, and a
report job merges their results.json files with the new
`benchmark.mjs --report=<dir>` mode. It defaults to a single
iteration and can also run the memory and builder-factor experiments
as their own jobs.
ng-packagr only accepts sources below the entry point's own folder, so
with --layout=single every folder now lives in <lib>/src/<folder>
instead of next to src. The default layout is unchanged.
tools/memory-experiments.mjs measures how much memory building the
entry-points apps needs from source vs incrementally: with Angular's
NG_BUILD_PARALLEL_TS/NG_BUILD_MAX_WORKERS knobs, under shrinking
--max-old-space-size limits, and with different Nx parallelism.

tools/builder-factors.mjs builds the same shared lib with both
builders four ways (secondary entry points or a single entry point,
SCSS or plain CSS) to isolate what makes ng-packagr slower.
SCSS makes ng-packagr much slower once a lib has many secondary entry
points (it sets up its Sass workers again for every entry point),
while @angular/build:library barely notices it. The entry-points-css
demo is the same 6 apps and 39 libs generated with --styles=css, so
the benchmark can show the difference end to end.

Each generated demo is selected by its own demo:<name> tag.
With no --max-old-space-size, building app0 from source runs out of
memory in the Angular compiler worker, while both incremental builds
of app0 succeed.
Rewrite the README around the three demos: what is measured and how,
build time and memory results locally and on CI, the out-of-memory
limits of building from source, the effect of Angular's thread
settings and Nx parallelism, what makes @angular/build:library faster
than ng-packagr, and when each approach is better or worse.
Every app's build already depends on ^build, so with
buildLibsFromSource: true a plain `nx build` (and CI's run-many) built
every lib and then compiled all of them again inside each app. Apps
now consume dist by default; the benchmark's from-source mode and the
memory experiments ask for buildLibsFromSource=true explicitly.
The single CI memory job lost its runner after 67 minutes, most likely
while building six apps from source three at a time on 16 GB, and
recorded nothing because results were only written at the end. The
script now saves after every build, and the workflow runs each memory
experiment as its own job. Document the failure and the default-heap
numbers in the README.
An empty demos input skips the benchmark and report jobs instead of
failing on an empty matrix.
Test three heap limits instead of four, drop the slow --parallel=1
from-source run, and move the from-source build with --parallel=3,
which exhausts a 16 GB runner, into its own memory-parallel-limit job.
Experiment jobs now time out after 30 minutes.
@ChristopherPHolder
ChristopherPHolder merged commit 7eab638 into main Oct 8, 2026
1 check failed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant