On this page

Performance

On a 5,000-component app, vite-plugin-ngp shows the first page 1.6 seconds after you start the dev server, where ng serve takes 28.4 seconds. A template edit reaches the page in 100 to 130 ms, where ng serve takes 1.8 seconds and Analog reloads the page in 2.7 seconds. Neither grows much with the app, where the CLI's times grow with every component, and the plugin type-checks the whole time. Once the page is up after a restart, the plugin's dev server holds 0.9 GB where ng serve holds 4.7 GB. @oxc-angular/vite matches the plugin's typical template edit and is faster and lighter at everything else, but it uses its own compiler, doesn't type-check, and its builds don't split @defer blocks into their own chunks. Production builds are where the plugin gains least: 20 seconds against ng build's 30, both type-checking, at about the same peak memory.

This page is for you if you're deciding whether to try vite-plugin-ngp instead of the Angular CLI or Analog, and want to know what you'd gain and what you'd give up. The apps here are generated; Benchmarks on real apps measures two open-source apps against their own CLI on GitHub's runners. Every number here was measured on one laptop on the evening of 17 October 2026, in one run of the benchmark in the repo's bench/ folder, on the plugin at commit e401c10. Each figure shows the medians its table lists; the plugin's bar is violet, every other tool's gray. The last sections say how it was measured, and where these numbers are weak.

What was measured#

Four toolchains, each with its own README's setup and nothing else, on the same apps, with the same Angular, 22.3.0-next.2, plus the plugin a second time with its opt-in typeChecker: 'tsrs', which swaps tsgo for Oxc's Rust port of TypeScript 7:

ToolchainVersionType-checks?
vite-plugin-ngpthe repo at commit e401c10yes: in a background process in dev, and before vite build finishes
vite-plugin-ngp, typeChecker: 'tsrs'the same, with tsrs 0.16.0-ts7.1.0-dev.20260929yes, the same check in tsrs
Angular CLI, ng serve and ng build@angular/build 22.3.0-next.2yes, in both
Analog, @analogjs/vite-plugin-angular2.8.2-beta.1, with a one-line patch (see below)no: disableTypeChecking is on by default
@oxc-angular/vite0.0.41no, by design

The three Vite plugins ran on Vite 8.3.4. The apps are generated Angular CLI workspaces of 100, 1,000 and 5,000 components, the same ones the ngp benchmark uses: standalone components with signals, control flow, @defer, pipes and directives, half of them with external templates. The fourth app is the repo's playground, 21 components. The Angular CLI can't run the playground, because it reads import.meta.env, which only Vite defines.

Each start and build ran once to warm up, then 5 measured times. Each edit ran twice to warm up, then 20 measured times. The tables show medians. A number marked ±N% varied by more than 5% from run to run, as median absolute deviation.

No number counts unless the app worked: the shell rendered every feature link and the first lazy route rendered its heading in headless Chrome, with no console errors, failed requests or exceptions. An edit counts only once the new content is in the DOM and still there 800 ms later. A build counts only if it boots and a second lazy route renders after a router navigation. Every one of these checks passed for every toolchain on every app. The one failure, in an earlier run, was Analog with its opt-in HMR turned on, described under Template edits.

Memory was recorded for every start, edit series and build: the resident memory of the tool's whole process tree, sampled every 100 ms. The Memory section has it.

Starting the dev server#

From starting the process to the first contentful paint of the rendered app.

0 s5 s10 s15 s20 s25 s100 componentsvite-plugin-ngpvite-plugin-ngp: median 784 ms of 5, fastest 758 ms, slowest 805 ms784 msAngular CLIAngular CLI: median 2.06 s of 5, fastest 2.03 s, slowest 2.10 s2.06 sAnalogAnalog: median 1.04 s of 5, fastest 1.03 s, slowest 1.06 s1.04 s@oxc-angular/vite@oxc-angular/vite: median 359 ms of 5, fastest 352 ms, slowest 365 ms359 ms1,000 componentsvite-plugin-ngpvite-plugin-ngp: median 1.00 s of 5, fastest 944 ms, slowest 1.01 s1.00 sAngular CLIAngular CLI: median 6.92 s of 5, fastest 6.83 s, slowest 6.97 s6.92 sAnalogAnalog: median 1.94 s of 5, fastest 1.94 s, slowest 1.98 s1.94 s@oxc-angular/vite@oxc-angular/vite: median 532 ms of 5, fastest 526 ms, slowest 538 ms532 ms5,000 componentsvite-plugin-ngpvite-plugin-ngp: median 1.64 s of 5, fastest 1.61 s, slowest 1.64 s1.64 sAngular CLIAngular CLI: median 28.4 s of 5, fastest 28.1 s, slowest 28.7 s28.4 sAnalogAnalog: median 6.43 s of 5, fastest 6.39 s, slowest 6.48 s6.43 s@oxc-angular/vite@oxc-angular/vite: median 1.09 s of 5, fastest 1.08 s, slowest 1.10 s1.09 sThe playground, 21 componentsvite-plugin-ngpvite-plugin-ngp: median 770 ms of 5, fastest 768 ms, slowest 783 ms770 msAngular CLIn/aAnalogAnalog: median 1.08 s of 5, fastest 1.05 s, slowest 1.08 s1.08 s@oxc-angular/vite@oxc-angular/vite: median 352 ms of 5, fastest 347 ms, slowest 393 ms352 ms
Cold start to first contentful paint: medians of 5 runs per tool and app, one laptop, 17 October 2026.

Toolchain1001,0005,000playground
vite-plugin-ngp784 ms1.00 s1.64 s770 ms
vite-plugin-ngp, typeChecker: 'tsrs'778 ms949 ms1.57 s775 ms
ng serve2.06 s6.92 s28.4 sn/a
Analog1.04 s1.94 s6.43 s1.08 s
@oxc-angular/vite359 ms532 ms1.09 s352 ms

That's a cold start: every cache the toolchain keeps was deleted first. A restart keeps those caches, and is what you do every morning:

0 s5 s10 s15 s20 s25 s100 componentsvite-plugin-ngpvite-plugin-ngp: median 405 ms of 5, fastest 402 ms, slowest 421 ms405 msAngular CLIAngular CLI: median 1.92 s of 5, fastest 1.91 s, slowest 1.99 s1.92 sAnalogAnalog: median 909 ms of 5, fastest 899 ms, slowest 937 ms909 ms@oxc-angular/vite@oxc-angular/vite: median 282 ms of 5, fastest 277 ms, slowest 284 ms282 ms1,000 componentsvite-plugin-ngpvite-plugin-ngp: median 453 ms of 5, fastest 446 ms, slowest 507 ms453 msAngular CLIAngular CLI: median 6.82 s of 5, fastest 6.74 s, slowest 6.85 s6.82 sAnalogAnalog: median 1.74 s of 5, fastest 1.72 s, slowest 1.76 s1.74 s@oxc-angular/vite@oxc-angular/vite: median 296 ms of 5, fastest 286 ms, slowest 318 ms296 ms5,000 componentsvite-plugin-ngpvite-plugin-ngp: median 570 ms of 5, fastest 568 ms, slowest 575 ms570 msAngular CLIAngular CLI: median 28.1 s of 5, fastest 27.8 s, slowest 28.5 s28.1 sAnalogAnalog: median 5.76 s of 5, fastest 5.75 s, slowest 5.87 s5.76 s@oxc-angular/vite@oxc-angular/vite: median 338 ms of 5, fastest 331 ms, slowest 339 ms338 msThe playground, 21 componentsvite-plugin-ngpvite-plugin-ngp: median 467 ms of 5, fastest 466 ms, slowest 491 ms467 msAngular CLIn/aAnalogAnalog: median 948 ms of 5, fastest 935 ms, slowest 965 ms948 ms@oxc-angular/vite@oxc-angular/vite: median 303 ms of 5, fastest 300 ms, slowest 313 ms303 ms
Restart to first contentful paint, with the caches kept: medians of 5 runs.

Toolchain1001,0005,000playground
vite-plugin-ngp405 ms453 ms570 ms467 ms
vite-plugin-ngp, typeChecker: 'tsrs'399 ms451 ms539 ms463 ms
ng serve1.92 s6.82 s28.1 sn/a
Analog909 ms1.74 s5.76 s948 ms
@oxc-angular/vite282 ms296 ms338 ms303 ms

ng serve and Analog compile the whole program before the first page, so their start grows with the app. The CLI's cache barely helps a restart: 28.1 against 28.4 seconds. vite-plugin-ngp compiles only what the first page imports, so at 5,000 components it's 17 times faster than ng serve cold and almost 50 times faster on a restart. Which process runs the type-check makes no difference you can see here: the tsrs rows are within the run-to-run drift of the default ones on every app.

Most of the plugin's cold start is Vite pre-bundling Angular's packages, which a restart skips: 1.1 seconds of the 1.6 at 5,000 components. That step runs Angular's Babel linker on every partially compiled package. @oxc-angular/vite pays 0.75 seconds for the same step, so the slower linker probably costs the plugin about 0.3 seconds there. That's an inference from the two differences, not a measurement of the linker itself.

The plugin's background type-check was on for all of these, as it is by default. Since 17 October the first check waits until the dev server has gone idle, so it no longer competes with the first page (Type-checking). In the afternoon run of the same day, before that change, the cold start at 5,000 components was 1.74 seconds against 1.64 here, and a back-to-back comparison on 16 October had put the check's cost to a cold start at about 150 ms. The two agree, but 100 ms is also the size of the drift between runs. A run on 18 October settled it: the plugin before and after the change, built as two packages and started in the same rounds on the 5,000-component app at a load under 4, painted cold in 1.84 against 1.70 seconds, every sample of the old plugin slower than every sample of the new, and restarted in 635 against 642 ms, which is inside the spread. A restart paints before the old one-second delay ever fired, so only the cold start had anything to gain (Type-checking).

Template edits#

From writing an external template to the new element in the page. "reload" means the toolchain reloaded the page to show it; every other edit updated the component in place. In brackets, the fastest edit.

0 s0.5 s1 s1.5 s2 s2.5 sfastest edit100 componentsvite-plugin-ngpvite-plugin-ngp: median 97 ms of 20, fastest 59 ms, slowest 125 msfastest: 59 ms97 ms ±30%Angular CLIAngular CLI: median 183 ms of 20, fastest 179 ms, slowest 228 msfastest: 179 ms183 msAnalogAnalog: median 329 ms of 20, fastest 227 ms, slowest 339 msfastest: 227 ms329 ms, reload@oxc-angular/vite@oxc-angular/vite: median 118 ms of 20, fastest 16 ms, slowest 120 msfastest: 16 ms118 ms1,000 componentsvite-plugin-ngpvite-plugin-ngp: median 119 ms of 20, fastest 56 ms, slowest 122 msfastest: 56 ms119 msAngular CLIAngular CLI: median 459 ms of 20, fastest 442 ms, slowest 506 msfastest: 442 ms459 msAnalogAnalog: median 789 ms of 20, fastest 633 ms, slowest 823 msfastest: 633 ms789 ms, reload@oxc-angular/vite@oxc-angular/vite: median 117 ms of 20, fastest 15 ms, slowest 119 msfastest: 15 ms117 ms5,000 componentsvite-plugin-ngpvite-plugin-ngp: median 109 ms of 20, fastest 60 ms, slowest 137 msfastest: 60 ms109 ms ±23%Angular CLIAngular CLI: median 1.81 s of 20, fastest 1.76 s, slowest 2.00 sfastest: 1.76 s1.81 sAnalogAnalog: median 2.72 s of 20, fastest 2.54 s, slowest 3.08 sfastest: 2.54 s2.72 s, reload@oxc-angular/vite@oxc-angular/vite: median 116 ms of 20, fastest 16 ms, slowest 120 msfastest: 16 ms116 msThe playground, 21 componentsvite-plugin-ngpvite-plugin-ngp: median 129 ms of 20, fastest 66 ms, slowest 132 msfastest: 66 ms129 msAngular CLIAnalogAnalog: median 229 ms of 20, fastest 195 ms, slowest 302 msfastest: 195 ms229 ms ±15%, reload@oxc-angular/vite@oxc-angular/vite: median 118 ms of 20, fastest 16 ms, slowest 121 msfastest: 16 ms118 ms
Template edit to the DOM: medians of 20 edits per tool and app, with the fastest edit marked by a ring.

Toolchain1001,0005,000playground
vite-plugin-ngp97 ms ±30% (59 ms)119 ms (56 ms)109 ms ±23% (60 ms)129 ms (66 ms)
vite-plugin-ngp, typeChecker: 'tsrs'123 ms (60 ms)102 ms ±19% (58 ms)71 ms ±11% (61 ms)129 ms (65 ms)
ng serve183 ms (179 ms)459 ms (442 ms)1.81 s (1.76 s)n/a
Analog, default329 ms (227 ms), reload789 ms (633 ms), reload2.72 s (2.54 s), reload229 ms ±15% (195 ms), reload
@oxc-angular/vite118 ms (16 ms)117 ms (15 ms)116 ms (16 ms)118 ms (16 ms)

The plugin's time stays flat from 100 to 5,000 components, because an edit regenerates only the edited component's update. ng serve rebuilds, and its time grows to 1.8 seconds. Analog reloads the page by default, and its time grows with the app too.

The fastest edit matters here because of macOS. In a test with no plugin at all, Vite's file watcher noticed a write after either about 10–35 ms or about 113 ms, unpredictably. Every Vite toolchain's times have that in them, so each toolchain's edits cluster around two values, and a median depends on how many of a run's edits got the slow report. That's why the plugin's median at 100 components is 97 ms with a ±30% spread and 119 ms at 1,000, and why its tsrs row reads 71 ms at 5,000 components where the default reads 109: more fast reports in those series, not a faster tool. The fastest edits, in brackets, are the same 56 to 66 ms in every plugin series.

The rings show what the medians hide. The plugin's edits cluster around 60 ms and 120 ms, and @oxc-angular/vite's, in the same run, around 16 ms and 118 ms. On a slow report the two tie. On a fast one, the plugin still waits: it holds a template update until 50 ms after the template was written, in case the same save also changes the component's class, and when the watcher reports within 10 ms about 40 ms of that window is left (How it works). Everything else, from the analyzer through ngp's update module to the message to the browser, takes about 5 ms. Until 17 October the wait counted from the watcher's report instead, and the plugin's edits clustered around 75 ms and 175 ms.

Analog's component HMR is off by default, and turning it on didn't work here, in a run on 16 October with the same Analog version. With liveReload: true, template edits on the playground updated in place in 262 ms (194 ms fastest). But on the generated apps, Analog logged an update for every template edit and the page never changed: the browser never asked for the component's new code. And on the playground, a .ts edit that changed a class field was sent as an update instead of a reload, so the page kept the old value. Those were the only checks that failed, so liveReload: true has no row in the tables.

.ts edits#

Changing a signal's initial value in the first route's page component. Every toolchain reloads the page for this, because a class change can't be applied to a running component.

0 s0.5 s1 s1.5 s2 s2.5 sfastest edit100 componentsvite-plugin-ngpvite-plugin-ngp: median 179 ms of 20, fastest 77 ms, slowest 182 msfastest: 77 ms179 msAngular CLIAngular CLI: median 236 ms of 20, fastest 228 ms, slowest 255 msfastest: 228 ms236 msAnalogAnalog: median 328 ms of 20, fastest 218 ms, slowest 331 msfastest: 218 ms328 ms@oxc-angular/vite@oxc-angular/vite: median 167 ms of 20, fastest 67 ms, slowest 170 msfastest: 67 ms167 ms1,000 componentsvite-plugin-ngpvite-plugin-ngp: median 187 ms of 20, fastest 85 ms, slowest 190 msfastest: 85 ms187 msAngular CLIAngular CLI: median 522 ms of 20, fastest 508 ms, slowest 658 msfastest: 508 ms522 msAnalogAnalog: median 781 ms of 20, fastest 627 ms, slowest 894 msfastest: 627 ms781 ms ±6%@oxc-angular/vite@oxc-angular/vite: median 171 ms of 20, fastest 67 ms, slowest 174 msfastest: 67 ms171 ms5,000 componentsvite-plugin-ngpvite-plugin-ngp: median 216 ms of 20, fastest 125 ms, slowest 290 msfastest: 125 ms216 ms ±17%Angular CLIAngular CLI: median 1.89 s of 20, fastest 1.85 s, slowest 2.04 sfastest: 1.85 s1.89 sAnalogAnalog: median 2.68 s of 20, fastest 2.59 s, slowest 3.07 sfastest: 2.59 s2.68 s@oxc-angular/vite@oxc-angular/vite: median 180 ms of 20, fastest 73 ms, slowest 186 msfastest: 73 ms180 msThe playground, 21 componentsvite-plugin-ngpvite-plugin-ngp: median 167 ms of 20, fastest 65 ms, slowest 170 msfastest: 65 ms167 msAngular CLIAnalogAnalog: median 201 ms of 20, fastest 192 ms, slowest 299 msfastest: 192 ms201 ms@oxc-angular/vite@oxc-angular/vite: median 162 ms of 20, fastest 61 ms, slowest 165 msfastest: 61 ms162 ms
.ts edit to the DOM: medians of 20 edits, with the fastest edit marked by a ring.

Toolchain1001,0005,000playground
vite-plugin-ngp179 ms (77 ms)187 ms (85 ms)216 ms ±17% (125 ms)167 ms (65 ms)
vite-plugin-ngp, typeChecker: 'tsrs'181 ms (79 ms)185 ms (76 ms)210 ms (117 ms)140 ms ±21% (64 ms)
ng serve236 ms (228 ms)522 ms (508 ms)1.89 s (1.85 s)n/a
Analog328 ms (218 ms)781 ms ±6% (627 ms)2.68 s (2.59 s)201 ms (192 ms)
@oxc-angular/vite167 ms (67 ms)171 ms (67 ms)180 ms (73 ms)162 ms (61 ms)

The reload includes fetching and running every module the page needs again. The plugin and @oxc-angular/vite are within the watcher's noise of each other here.

Type errors in the dev server#

Only vite-plugin-ngp and ng serve type-check in the dev server. This times planting a template type error until the terminal names it, and then removing it until the terminal reports the app clean. The plugin is here twice: with tsgo, the default, and with tsrs.

Toolchain1001,0005,000playground
vite-plugin-ngp, error shown496 ms638 ms1.36 s500 ms
vite-plugin-ngp, error cleared502 ms548 ms1.36 s498 ms
with tsrs, error shown484 ms557 ms1.02 s479 ms
with tsrs, error cleared483 ms500 ms ±6%1.02 s477 ms
ng serve, error shown182 ms433 ms1.66 sn/a
ng serve, error cleared185 ms455 ms1.78 sn/a

The plugin waits 300 ms after an edit before it checks, so on small apps ng serve reports first. At 5,000 components the plugin's whole-program tsgo check takes about 1.4 seconds, a little faster than ng serve's rebuild. Neither holds up the page: the plugin's template update arrives in about 110 ms, and the type error 1.4 seconds after the edit. With typeChecker: 'tsrs' the same error shows in 1.02 seconds, a third less, and at 1,000 components in 557 ms against 638. On the small apps the two are within 20 ms, because there the 300 ms wait and the watcher's delay are most of the half second and the check itself is a tenth of it.

Production builds#

From starting vite build or ng build to the process exiting, with caches deleted first.

0 s10 s20 s30 s100 componentsvite-plugin-ngpvite-plugin-ngp: median 1.32 s of 5, fastest 1.31 s, slowest 1.33 s1.32 svite-plugin-ngp, typeCheck: falsevite-plugin-ngp, typeCheck: false: median 1.16 s of 5, fastest 1.15 s, slowest 1.19 s1.16 sAngular CLIAngular CLI: median 2.01 s of 5, fastest 2.00 s, slowest 2.04 s2.01 sAnalogAnalog: median 1.44 s of 5, fastest 1.44 s, slowest 1.48 s1.44 s@oxc-angular/vite@oxc-angular/vite: median 262 ms of 5, fastest 259 ms, slowest 262 ms262 ms1,000 componentsvite-plugin-ngpvite-plugin-ngp: median 4.79 s of 5, fastest 4.72 s, slowest 4.89 s4.79 svite-plugin-ngp, typeCheck: falsevite-plugin-ngp, typeCheck: false: median 3.88 s of 5, fastest 3.86 s, slowest 3.91 s3.88 sAngular CLIAngular CLI: median 6.73 s of 5, fastest 6.71 s, slowest 6.80 s6.73 sAnalogAnalog: median 4.51 s of 5, fastest 4.49 s, slowest 4.52 s4.51 s@oxc-angular/vite@oxc-angular/vite: median 441 ms of 5, fastest 437 ms, slowest 451 ms441 ms5,000 componentsvite-plugin-ngpvite-plugin-ngp: median 20.5 s of 5, fastest 20.3 s, slowest 21.5 s20.5 svite-plugin-ngp, typeCheck: falsevite-plugin-ngp, typeCheck: false: median 16.7 s of 5, fastest 16.7 s, slowest 16.8 s16.7 sAngular CLIAngular CLI: median 30.2 s of 5, fastest 29.9 s, slowest 30.7 s30.2 sAnalogAnalog: median 19.3 s of 5, fastest 18.4 s, slowest 19.3 s19.3 s@oxc-angular/vite@oxc-angular/vite: median 1.45 s of 5, fastest 1.42 s, slowest 1.46 s1.45 sThe playground, 21 componentsvite-plugin-ngpvite-plugin-ngp: median 1.08 s of 5, fastest 1.07 s, slowest 1.11 s1.08 svite-plugin-ngp, typeCheck: falsevite-plugin-ngp, typeCheck: false: median 1.03 s of 5, fastest 1.02 s, slowest 1.04 s1.03 sAngular CLIn/aAnalogAnalog: median 1.22 s of 5, fastest 1.21 s, slowest 1.25 s1.22 s@oxc-angular/vite@oxc-angular/vite: median 317 ms of 5, fastest 316 ms, slowest 321 ms317 ms
Production build, process start to exit: medians of 5 builds per tool and app.

Toolchain1001,0005,000playground
vite-plugin-ngp1.32 s4.79 s20.5 s1.08 s
vite-plugin-ngp, typeChecker: 'tsrs'1.29 s4.65 s19.6 s1.07 s
vite-plugin-ngp, typeCheck: false1.16 s3.88 s16.7 s1.03 s
ng build2.01 s6.73 s30.2 sn/a
Analog1.44 s4.51 s19.3 s1.22 s
@oxc-angular/vite262 ms441 ms1.45 s317 ms

The fair comparisons are the plugin against ng build, since both type-check, and the plugin without type-checking against Analog and @oxc-angular/vite, which don't. Against ng build, the plugin is about a third faster. Without type-checking it comes in under Analog by 2.6 seconds at 5,000 components. @oxc-angular/vite builds 5,000 components in 1.4 seconds, about 12 times faster than anything else, because its compiler is Rust from end to end. ngp's analyzer is Rust too, but its code generation is Angular's template compiler, in TypeScript. In a build of the 5,000-component app without type-checking, profiled on 16 October, 14.6 of 16.6 seconds went to transforming modules, and Rolldown's rendering and minifying took the remaining 2. Rolldown's own timings put the plugin's build optimizer, which runs Babel on every module, at 3.6 seconds of that. The rest of the transform time is mostly ngp. The type-check adds another 3.8 seconds at 5,000 components with tsgo, and 2.9 with tsrs; it runs while Rolldown renders, so it's partly hidden.

Bundle size#

All the JavaScript a build writes, and the initial part: what index.html loads plus everything it imports statically, before any lazy route. Gzip at level 9, per file. The figures show the gzipped sizes; the table has the raw ones too.

0 MB0.5 MB1 MB1.5 MB2 MB100 componentsvite-plugin-ngpvite-plugin-ngp: 127 kB gzipped, 424 kB raw, 47 files127 kBAngular CLIAngular CLI: 128 kB gzipped, 421 kB raw, 56 files128 kBAnalogAnalog: 127 kB gzipped, 423 kB raw, 47 files127 kB@oxc-angular/vite@oxc-angular/vite: 131 kB gzipped, 499 kB raw, 15 files131 kB1,000 componentsvite-plugin-ngpvite-plugin-ngp: 525 kB gzipped, 2,017 kB raw, 392 files525 kBAngular CLIAngular CLI: 523 kB gzipped, 1,994 kB raw, 464 files523 kBAnalogAnalog: 521 kB gzipped, 2,006 kB raw, 393 files521 kB@oxc-angular/vite@oxc-angular/vite: 381 kB gzipped, 1,961 kB raw, 181 files381 kB5,000 componentsvite-plugin-ngpvite-plugin-ngp: 2,342 kB gzipped, 9,296 kB raw, 1884 files2,342 kBAngular CLIAngular CLI: 2,275 kB gzipped, 9,080 kB raw, 2223 files2,275 kBAnalogAnalog: 2,324 kB gzipped, 9,237 kB raw, 1894 files2,324 kB@oxc-angular/vite@oxc-angular/vite: 1,506 kB gzipped, 8,570 kB raw, 892 files1,506 kBThe playground, 21 componentsvite-plugin-ngpvite-plugin-ngp: 135 kB gzipped, 403 kB raw, 13 files135 kBAngular CLIn/aAnalogAnalog: 134 kB gzipped, 406 kB raw, 13 files134 kB@oxc-angular/vite@oxc-angular/vite: 157 kB gzipped, 490 kB raw, 12 files157 kB
All the JavaScript a production build writes, gzipped, per tool and app: the output of the last of the 5 measured builds.

0 kB50 kB100 kB100 componentsvite-plugin-ngpvite-plugin-ngp: 71 kB gzipped, 232 kB raw, 1 file71 kBAngular CLIAngular CLI: 77 kB gzipped, 247 kB raw, 1 file77 kBAnalogAnalog: 71 kB gzipped, 232 kB raw, 1 file71 kB@oxc-angular/vite@oxc-angular/vite: 104 kB gzipped, 343 kB raw, 1 file104 kB1,000 componentsvite-plugin-ngpvite-plugin-ngp: 76 kB gzipped, 246 kB raw, 1 file76 kBAngular CLIAngular CLI: 78 kB gzipped, 251 kB raw, 1 file78 kBAnalogAnalog: 76 kB gzipped, 246 kB raw, 1 file76 kB@oxc-angular/vite@oxc-angular/vite: 109 kB gzipped, 356 kB raw, 1 file109 kB5,000 componentsvite-plugin-ngpvite-plugin-ngp: 97 kB gzipped, 313 kB raw, 2 files97 kBAngular CLIAngular CLI: 81 kB gzipped, 268 kB raw, 1 file81 kBAnalogAnalog: 97 kB gzipped, 314 kB raw, 2 files97 kB@oxc-angular/vite@oxc-angular/vite: 129 kB gzipped, 422 kB raw, 1 file129 kBThe playground, 21 componentsvite-plugin-ngpvite-plugin-ngp: 115 kB gzipped, 354 kB raw, 1 file115 kBAngular CLIn/aAnalogAnalog: 114 kB gzipped, 355 kB raw, 1 file114 kB@oxc-angular/vite@oxc-angular/vite: 137 kB gzipped, 443 kB raw, 1 file137 kB
The initial JavaScript, gzipped: what the browser downloads before any lazy route.

AppToolchainAll JSgzipInitial JSgzip
100vite-plugin-ngp424 kB127 kB232 kB71 kB
100ng build421 kB128 kB247 kB77 kB
100Analog423 kB127 kB232 kB71 kB
100@oxc-angular/vite499 kB131 kB343 kB104 kB
1,000vite-plugin-ngp2,017 kB525 kB246 kB76 kB
1,000ng build1,994 kB523 kB251 kB78 kB
1,000Analog2,006 kB521 kB246 kB76 kB
1,000@oxc-angular/vite1,961 kB381 kB356 kB109 kB
5,000vite-plugin-ngp9,296 kB2,342 kB313 kB97 kB
5,000ng build9,080 kB2,275 kB268 kB81 kB
5,000Analog9,237 kB2,324 kB314 kB97 kB
5,000@oxc-angular/vite8,570 kB1,506 kB422 kB129 kB
playgroundvite-plugin-ngp403 kB135 kB354 kB115 kB
playgroundAnalog406 kB134 kB355 kB114 kB
playground@oxc-angular/vite490 kB157 kB443 kB137 kB

The plugin's output is within 3% of ng build's in total, gzipped, at every size, because it runs the CLI's build optimizer on Angular's own compiler output. At 5,000 components its initial JavaScript is 16 kB larger gzipped than ng build's. Analog's is the same size as the plugin's, and both bundle with Rolldown, so that's likely Rolldown's chunking rather than the compiler.

@oxc-angular/vite's total looks a third smaller at 5,000 components, and that's not a saving. Its build doesn't give @defer blocks their own chunks: the deferred component's code sits in its route's chunk, so the build writes 892 files where the plugin writes 1,884, and fewer, larger files compress better. Its initial JavaScript is 33–46% larger than the plugin's on the generated apps, gzipped, and 19% on the playground.

The playground's builds carry the $localize runtime, about 20 kB, 8 kB gzipped, because its copy in the benchmark imports it the way ng add @angular/localize would, so that all three toolchains run the same code. The plugin's own i18n option drops it (Internationalization).

Memory#

The resident memory of each toolchain's processes, summed over its process tree and sampled every 100 ms: the Vite or CLI process plus what it spawned, which is esbuild, tsgo, Sass's Dart VM and worker processes, depending on the tool. Shared pages count once per process, so a sum is an upper bound. It's what top would add up, and the number a laptop runs out of. Headless Chrome isn't in it.

0 GB1 GB2 GB3 GB4 GB100 componentsvite-plugin-ngpvite-plugin-ngp: median 360 MB of 5, fastest 357 MB, slowest 371 MB360 MBAngular CLIAngular CLI: median 784 MB of 5, fastest 779 MB, slowest 795 MB784 MBAnalogAnalog: median 452 MB of 5, fastest 451 MB, slowest 453 MB452 MB@oxc-angular/vite@oxc-angular/vite: median 218 MB of 5, fastest 203 MB, slowest 221 MB218 MB1,000 componentsvite-plugin-ngpvite-plugin-ngp: median 551 MB of 5, fastest 550 MB, slowest 552 MB551 MBAngular CLIAngular CLI: median 1.81 GB of 5, fastest 1.78 GB, slowest 1.83 GB1.81 GBAnalogAnalog: median 615 MB of 5, fastest 614 MB, slowest 620 MB615 MB@oxc-angular/vite@oxc-angular/vite: median 218 MB of 5, fastest 209 MB, slowest 229 MB218 MB5,000 componentsvite-plugin-ngpvite-plugin-ngp: median 932 MB of 5, fastest 919 MB, slowest 943 MB932 MBAngular CLIAngular CLI: median 4.67 GB of 5, fastest 4.15 GB, slowest 4.93 GB4.67 GBAnalogAnalog: median 1.34 GB of 5, fastest 1.33 GB, slowest 1.34 GB1.34 GB@oxc-angular/vite@oxc-angular/vite: median 261 MB of 5, fastest 252 MB, slowest 267 MB261 MBThe playground, 21 componentsvite-plugin-ngpvite-plugin-ngp: median 336 MB of 5, fastest 331 MB, slowest 340 MB336 MBAngular CLIn/aAnalogAnalog: median 480 MB of 5, fastest 470 MB, slowest 489 MB480 MB@oxc-angular/vite@oxc-angular/vite: median 205 MB of 5, fastest 204 MB, slowest 223 MB205 MB
Dev server memory after a restart, once the page had rendered and settled: medians of 5 runs.

What the dev server held once the page had rendered after a restart, and in brackets its peak between the process start and that point:

Toolchain1001,0005,000playground
vite-plugin-ngp360 MB (420 MB ±6% peak)551 MB (685 MB peak)932 MB (932 MB peak)336 MB (348 MB peak)
vite-plugin-ngp, typeChecker: 'tsrs'368 MB (368 MB peak)556 MB (563 MB peak)939 MB (939 MB peak)335 MB (347 MB peak)
ng serve784 MB (784 MB peak)1.81 GB (1.81 GB peak)4.67 GB (5.65 GB peak)n/a
Analog452 MB (452 MB peak)615 MB (615 MB peak)1.34 GB (1.34 GB peak)480 MB (480 MB peak)
@oxc-angular/vite218 MB (218 MB peak)218 MB (218 MB peak)261 MB (261 MB peak)205 MB (205 MB peak)

After the edit series, 44 edits in one session, and in brackets the peak during them:

Toolchain1001,0005,000playground
vite-plugin-ngp321 MB (465 MB peak)517 MB (828 MB peak)1.67 GB (2.48 GB peak)319 MB (436 MB peak)
vite-plugin-ngp, typeChecker: 'tsrs'321 MB (420 MB peak)532 MB (673 MB peak)1.68 GB (2.01 GB peak)316 MB (414 MB peak)
ng serve861 MB (865 MB peak)1.85 GB (1.90 GB peak)6.07 GB (6.44 GB peak)n/a
Analog476 MB (532 MB peak)1.08 GB (1.18 GB peak)3.52 GB (3.60 GB peak)480 MB (497 MB peak)
@oxc-angular/vite176 MB (229 MB peak)176 MB (225 MB peak)239 MB (263 MB peak)193 MB (219 MB peak)

A cold start peaks higher for every tool, because Vite's pre-bundling and the CLI's first compile run then: 1.56 GB for the plugin at 5,000 components, 5.63 GB for ng serve, 2.01 GB for Analog and 977 MB for @oxc-angular/vite.

0 GB1 GB2 GB3 GB4 GB100 componentsvite-plugin-ngpvite-plugin-ngp: median 644 MB of 5, fastest 612 MB, slowest 678 MB644 MBAngular CLIAngular CLI: median 779 MB of 5, fastest 764 MB, slowest 796 MB779 MBAnalogAnalog: median 672 MB of 5, fastest 663 MB, slowest 716 MB672 MB@oxc-angular/vite@oxc-angular/vite: median 280 MB of 5, fastest 276 MB, slowest 282 MB280 MB1,000 componentsvite-plugin-ngpvite-plugin-ngp: median 1.46 GB of 5, fastest 1.45 GB, slowest 1.59 GB1.46 GBAngular CLIAngular CLI: median 1.50 GB of 5, fastest 1.45 GB, slowest 1.52 GB1.50 GBAnalogAnalog: median 1.12 GB of 5, fastest 1.10 GB, slowest 1.13 GB1.12 GB@oxc-angular/vite@oxc-angular/vite: median 576 MB of 5, fastest 569 MB, slowest 583 MB576 MB5,000 componentsvite-plugin-ngpvite-plugin-ngp: median 4.77 GB of 5, fastest 4.14 GB, slowest 5.18 GB4.77 GB ±7%Angular CLIAngular CLI: median 4.61 GB of 5, fastest 4.55 GB, slowest 5.06 GB4.61 GBAnalogAnalog: median 3.39 GB of 5, fastest 3.36 GB, slowest 3.40 GB3.39 GB@oxc-angular/vite@oxc-angular/vite: median 1.55 GB of 5, fastest 1.52 GB, slowest 1.56 GB1.55 GBThe playground, 21 componentsvite-plugin-ngpvite-plugin-ngp: median 624 MB of 5, fastest 610 MB, slowest 636 MB624 MBAngular CLIn/aAnalogAnalog: median 648 MB of 5, fastest 639 MB, slowest 661 MB648 MB@oxc-angular/vite@oxc-angular/vite: median 270 MB of 5, fastest 264 MB, slowest 274 MB270 MB
Peak memory of a production build: medians of 5 builds.

Toolchain1001,0005,000playground
vite-plugin-ngp644 MB1.46 GB4.77 GB ±7%624 MB
vite-plugin-ngp, typeChecker: 'tsrs'643 MB1.26 GB4.39 GB612 MB
vite-plugin-ngp, typeCheck: false610 MB1.32 GB3.76 GB566 MB
ng build779 MB1.50 GB4.61 GBn/a
Analog672 MB1.12 GB3.39 GB648 MB
@oxc-angular/vite280 MB576 MB1.55 GB270 MB

The dev server is where the difference is. ng serve holds the whole compiled program, so it grows with the app, to 4.7 GB at 5,000 components after a restart, 5.7 at its peak on the way there, and 6.1 GB after the edits. The plugin holds what the browser has asked for: 0.9 GB after a restart at 5,000 components, less than Analog's 1.3 GB for the same page without a type-checker, and 1.7 GB after the edits where Analog holds 3.5 GB. Both peak higher during the edits, the plugin at 2.5 GB and Analog at 3.6. The plugin's check runs again after each edit, and the harness doesn't separate its share from Vite's, but the tsrs row does: with tsrs the same edits peak at 2.0 GB, so about half a gigabyte of that peak is tsgo. @oxc-angular/vite holds a quarter of a gigabyte at every size, with no type-checker and a compiler that keeps no program.

Builds are even. The plugin's build of the 5,000-component app peaks at 4.8 GB against ng build's 4.6, and 3.8 GB without the type-check, a little above Analog's 3.4. The gigabyte between those two plugin rows is tsgo checking the program next to Rolldown: the harness can't separate the two processes, but the type-check is the only difference between the rows. With tsrs the same build peaks at 4.4 GB.

What it adds up to#

If your app is large and you use ng serve, the dev server is where the plugin changes the most: a dev server that's ready in under 2 seconds instead of half a minute, template edits in a tenth of the time, and a fifth of the memory, with type-checking kept. If you use Analog, the plugin starts almost 4 times faster at 5,000 components and updates templates in place by default, where Analog reloads. Builds improve less: a third faster than ng build with the same output size and about the same peak memory. Checking with tsrs instead of tsgo changes none of that, and takes a third off the wait for a type error on a large app.

@oxc-angular/vite beats the plugin on every timing here except the typical template edit, where they tie, and holds a quarter of its memory. What you give up for that is Angular's own compiler, type-checking, and code-splitting for @defer. The plugin's output is Angular's compiler's output, byte for byte the same as a full ngp build.

Where these numbers are weak#

  • One laptop, with other work running. An Apple M2 Max with 12 cores and 32 GB, on macOS 27, Node 26.12.0, headless Chrome 155. An idle virtual machine held one core for the whole run, and other development sessions' dev servers were on the same machine. Each start and build waited for the 1-minute load average to drop below 6, and samples recorded loads between 2.0 and 8.8. Waiting for the load keeps samples out of the worst spikes, not out of all of them, and starts and builds ran in rounds, one toolchain after another, so a drift lands on every toolchain in a table alike. Compare toolchains within a table, not with numbers from another run or machine: the same measurement drifts between runs, and this run's numbers differ from the afternoon run of the same day by a few percent either way, in both directions.
  • Generated apps. They're realistic in shape, but they're not your app: no third-party libraries beyond Angular, no large stylesheets, a global stylesheet of a few lines. The playground is the only hand-written app, and it's small. Benchmarks on real apps has Super Productivity and Ghostfolio.
  • Cold means the toolchains' caches, not the operating system's. The file system cache was warm for every run. A first start after a reboot would be slower for everyone.
  • One kind of edit each. Adding an element to a template, and changing a string in a class. An edit that adds an import or a new component can cost more, and wasn't measured.
  • The watcher's noise is in every Vite number. Up to about 100 ms per edit, on macOS. Linux uses a different watcher and wasn't measured here; the real apps ran on Ubuntu runners.
  • Memory is an upper bound. Resident set sizes summed over processes count shared pages more than once, and a 100 ms sample can miss a shorter peak.
  • Analog was patched. Its latest stable release, 2.8.1, can't build on Angular 22.3: its build optimizer calls a private @angular/build API that now needs an initialization call. 2.8.2-beta.1 adds that call, behind a version check that reads Angular's prerelease version as NaN and so still fails. A one-line parseInt patch fixes the check. On a stable Angular release, the beta should work unpatched. Not measured.
  • @oxc-angular/vite targets Angular 22.2.2. It doesn't use Angular's compiler, so the Angular version only matters at runtime here, and every app rendered and passed every check.

Run it yourself#

The harness is in bench/ in the repo, with a README on how to run it: one command sets up the apps, one runs the benchmark, one prints these tables from its result files, and one draws the figures. The result files behind this page are in bench/results/, with the machine, versions, commit and load average of every sample.

Last updated . View as markdown.