Angular deprecated its webpack builders in v22, yet webpack is still the only way Cypress compiles Angular component tests out of the box. Cypress also runs on Vite, though, and Vite has several Angular toolchains, including a new one I wanted to try: OXC’s experimental Angular compiler, written in Rust.
So I ran the same 41 component tests through every toolchain I could plug into Cypress: webpack, Analog, @angular/build, OXC, and SWC with the @jscutlery/swc-angular preset I maintain. With JIT and AOT where they support both, plus Analog’s new fastCompile mode, that makes eight setups on Angular 22, compared on run time, memory, and whether the tests still pass.
Angular 22 Cypress component testing: webpack vs Analog vs SWC vs OXC compiler benchmark
The five toolchains
Same specs, same components. Only the toolchain that compiles them changes:
| Toolchain | Package | Dev server | Template compiler | Modes |
|---|---|---|---|---|
| webpack | @ngtools/webpack, through Cypress’s built-in handler | webpack | Angular’s | JIT |
| Analog | @analogjs/vite-plugin-angular | Vite | Angular’s | JIT, AOT |
| @angular/build | @angular/build, through an Analog option | Vite | Angular’s | JIT, AOT |
| OXC | @oxc-angular/vite | Vite | OXC’s, in Rust | AOT |
| SWC | unplugin-swc with the @jscutlery/swc-angular preset | Vite | Angular’s | JIT |
Analog also has a fastCompile option, a single-pass compiler that skips the TypeScript program. I ran it in AOT as an eighth setup, Analog fast in the charts.
That’s only two template compilers: Angular’s, driven five different ways, and OXC’s. webpack is the baseline: it’s what Cypress ships for Angular, and Cypress forces JIT for it (aot: false).
Why no esbuild toolchain? Cypress has no esbuild dev server for Angular component testing (cypress-io/cypress#30903). The @angular/build setups are the closest stand-in: the CLI’s compilation pipeline inside Cypress’s Vite dev server. They measure Angular’s compiler, not the CLI’s bundler.
How each toolchain works
Each toolchain spreads the work over three places: the dev server at startup, the dev server for each file the browser requests, and the browser itself. Where the templates get compiled depends on the mode. In AOT, the dev server compiles them. In JIT, the code keeps its decorators, and Angular compiles each template in the browser the first time its component mounts.
Top to bottom, from the dev server to the browser. Stacked steps cover the whole project at once, the others one file at a time. Hover a step for details.
| Toolchain | Dev server, at startup | Dev server, per file | Browser, on mount |
|---|---|---|---|
| webpack JIT | Nothing | Type-check the project (whole project), Bundle the spec | Compile templates, Run the test |
| Analog, @angular/build JIT | TypeScript program (whole project) | Serve the file | Compile templates, Run the test |
| Analog, @angular/build AOT | TypeScript program (whole project), Compile templates (whole project) | Serve the file | Run the test |
| Analog, fastCompile AOT | Scan the project (whole project) | Compile templates | Run the test |
| SWC JIT | Nothing | Strip types | Compile templates, Run the test |
| OXC AOT | Nothing | Strip types, Compile templates | Run the test |
Two kinds of steps cost time, and they grow with the project. A stacked step covers the whole project before the first test runs: Analog and @angular/build pay it at startup, webpack when the first spec is requested. Analog fast swaps the TypeScript program for a lighter scan, but it still reads the whole project. A template compile in the browser runs inside the tests, once per component: every JIT setup pays it.
OXC is the only toolchain with neither. SWC skips the stacked step but compiles in the browser. The next sections measure both costs.
How I measured
- Angular
- 22.2.0TypeScript 6.0
- Cypress
- 15.17.0Electron 37, headless
- Vite
- 7.3.6same config for every Vite setup
- Machine
- Ryzen 9 9950X3D16 cores, 64 GB
The suite has 12 app specs, testing components like the ones in a real app (a form, a dialog, a list loaded over HTTP, the router), and 3 stress specs that render hundreds of generated components at once to load the compiler.
The charts switch between three scenarios: App + stress (all 15 specs, 41 tests, with 300 generated components), and the stress specs alone with 300 or 1000 generated components (Stress, 300 and Stress, 1000). Each result is the median of 3 runs, from a cold cache, one Cypress at a time.
Known issues
Some setups have known issues. I kept them out of the timings, so every setup runs the same tests:
One spec per known issue, run once per setup and never timed.
What each one means:
overrideComponent(Analog AOT, @angular/build AOT): a test can’t swap a component’s template or providers. Analog only keeps the metadata TestBed needs when it detects a test run, and it doesn’t recognise Cypress as one. Running Cypress withNODE_ENV=testfixes it: both setups then pass.overrideComponent(Analog fast): overriding providers or the template works, but the recompiled component loses its inputs.fastCompiledrops the input metadata TestBed needs, even withNODE_ENV=test(analogjs/analog#2666).- OXC #585: an event handler that reads a
@foritem or an@if … asalias through$safeNavigationMigration(…)receivesnullinstead of the value. - OXC #586: on Angular before 22.2.2, an image inside a library component never loads when its URL is a
SafeUrl.
Some limits don’t show up in a test:
- webpack needs a pnpm override on Angular 22, because Cypress rejects webpack-dev-server 6 (#34259). Before Cypress 15.20.1 it also needed a shim for Babel 8 (#34461). Angular deprecated its webpack builders in v22.
- @angular/build is only reachable through an experimental Analog option.
- OXC is still at 0.0.41 and officially requires Vite 8. It ignores
useDefineForClassFields: false(#73), doesn’t type-check, and loads@deferblocks eagerly (#313). - SWC needs the
reflect-metadatapackage for dependency injection.
Total run time
From spawning Cypress to its exit. Switch to Stress, 1000 to see the compilers pull apart:
From starting Cypress to its exit. Median of 3 cold runs, fastest first; the percentage compares with webpack.
| Setup | Wall time | Startup | Test bodies | Compared with webpack |
|---|---|---|---|---|
| OXC AOT | 26.8 s | 3.4 s | 4.7 s | −22% |
| SWC JIT | 27.2 s | 3.4 s | 5.4 s | −20% |
| Analog fast AOT | 28.5 s | 5.2 s | 4.7 s | −17% |
| Analog JIT | 29.4 s | 5.8 s | 5.5 s | −14% |
| @angular/build JIT | 29.5 s | 5.9 s | 5.5 s | −14% |
| Analog AOT | 29.6 s | 6.3 s | 4.7 s | −13% |
| @angular/build AOT | 29.7 s | 6.5 s | 4.7 s | −13% |
| webpack JIT | 34.1 s | 7.3 s | 5.5 s | ±0% |
OXC is the fastest in every scenario, and its lead grows with the project: with 1000 components, it takes 60% less time than webpack.
Setups change rank as the project grows. SWC keeps up with OXC on App + stress, then falls to third with 1000 components, behind Analog fast. Analog is no faster in AOT than in JIT on App + stress, then pulls ahead with 1000 components. The next two sections explain why.
Time to the first test
The startup covers Cypress boot, the dev server, dependency pre-bundling and the first spec’s compile:
Cypress boot, dev server start, dependency pre-bundling and compiling the first spec. Median of 3 cold runs on the stress specs.
| Setup | 300 components | 1000 components |
|---|---|---|
| OXC AOT | 3.5 s | 4.4 s |
| SWC JIT | 3.8 s | 5.2 s |
| Analog fast AOT | 5.3 s | 7.6 s |
| @angular/build JIT | 6.2 s | 8.3 s |
| Analog JIT | 6.0 s | 8.3 s |
| Analog AOT | 6.2 s | 8.9 s |
| @angular/build AOT | 6.6 s | 9.1 s |
| webpack JIT | 8.0 s | 10.6 s |
OXC and SWC transform files one at a time, on request, so their startup grows the least: 0.9 s and 1.3 s more at 1000 components. Analog builds a TypeScript program of the whole project before serving the first file: the bigger the project, the longer the wait, up to 2.7 s more. Analog fast skips the program but still scans the whole project, and grows almost as much (2.4 s).
Split into phases, the gap is the dev server start:
Phases between Cypress's own events, median of 3 cold runs. The percentage compares with the fastest.
| Setup | Cypress boot | Dev server start | Browser launch | First spec: compile and load |
|---|---|---|---|---|
| OXC AOT | 0.8 s | 1.9 s | 0.7 s | 1.0 s |
| SWC JIT | 0.8 s | 1.8 s | 0.7 s | 1.9 s |
| Analog fast AOT | 0.8 s | 5.2 s | 0.7 s | 1.0 s |
| @angular/build JIT | 0.8 s | 4.9 s | 0.7 s | 1.9 s |
| Analog JIT | 0.8 s | 5.0 s | 0.7 s | 1.9 s |
| Analog AOT | 0.8 s | 6.5 s | 0.7 s | 1.0 s |
| @angular/build AOT | 0.8 s | 6.7 s | 0.7 s | 1.0 s |
| webpack JIT | 0.8 s | 3.3 s | 0.7 s | 5.8 s |
OXC and SWC have their dev server ready in under 2 s. The Analog-based setups spend that time analyzing the project, and webpack loses it later, compiling the first spec on demand.
JIT slows down the tests
JIT setups compile each template in the browser, the first time a component mounts. That compile runs inside the tests:
Time spent inside the tests of the 3 stress specs. Median of 3 cold runs.
| Setup | Test bodies |
|---|---|
| @angular/build AOT | 0.9 s |
| Analog fast AOT | 1.0 s |
| Analog AOT | 1.0 s |
| OXC AOT | 1.0 s |
| @angular/build JIT | 2.4 s |
| SWC JIT | 2.4 s |
| Analog JIT | 2.4 s |
| webpack JIT | 2.5 s |
With 1000 components, the JIT test bodies take almost 150% longer than the AOT ones. AOT setups did that work in the dev server, before any test ran.
That’s why SWC doesn’t win at scale: it gives its fast startup back in the browser. With 1000 components, it still beats the regular Analog and @angular/build AOT setups. OXC is the only setup with both a fast startup and AOT output.
Memory use
Peak PSS of the whole process tree: the CLI, Electron and the dev server. It’s sampled every second, so on runs this short the peaks are approximate. PSS splits the pages Electron’s processes share, where RSS counts them once per process.
Peak PSS, sampled every second across the CLI, Electron and the dev server. Median of 3 cold runs.
| Setup | Peak PSS |
|---|---|
| Analog fast AOT | 1.88 GB |
| SWC JIT | 2.02 GB |
| Analog JIT | 2.25 GB |
| OXC AOT | 2.29 GB |
| Analog AOT | 2.34 GB |
| @angular/build JIT | 2.41 GB |
| @angular/build AOT | 2.68 GB |
| webpack JIT | 3.43 GB |
webpack needs the most memory, up to 3.7 GB at the peak. Among the Vite setups, Analog fast is the lightest, probably because it holds no TypeScript program in memory, followed by SWC. @angular/build is the heaviest. On CI, that decides how many Cypress processes fit on one machine.
Conclusion
Any Vite setup beats webpack. On App + stress they take 13 to 22% less time, and the gap grows with the project. webpack is also the heaviest, and Angular deprecated its webpack builders. The only reason to keep it is that it’s what Cypress ships.
OXC is the fastest in every scenario: with 1000 components, it takes 60% less time than webpack. It’s also the youngest: version 0.0.41, officially Vite 8 only, no type-checking, and it fails two of the three known issues. Try it on a branch, and keep another setup ready to switch back to.
Analog AOT is the safe choice. It runs Angular’s own compiler and passes every known issue once Cypress runs with NODE_ENV=test. Its startup grows with the project, but with 1000 components it still takes 39% less time than webpack.
Analog fast is the one to watch. With 1000 components it comes second, taking 45% less time than webpack, and it’s the lightest setup of all. It keeps Angular’s own compiler, so OXC’s bugs don’t apply. But it’s new and opt-in, it still scans the whole project at startup, and overrideComponent loses inputs (analogjs/analog#2666).
SWC passes every known issue without any flag. It pays for JIT in the browser as the project grows, but with 1000 components it still takes 42% less time than webpack, third behind OXC and Analog fast.
Every run, spec and memory sample is in the sandbox repository, with the full results. If your specs are bigger, run the bench on your own components and tell me what you get.