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.

edbzn/cct-compiler-sandbox

Angular 22 Cypress component testing: webpack vs Analog vs SWC vs OXC compiler benchmark

TypeScript0

The five toolchains

Same specs, same components. Only the toolchain that compiles them changes:

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).

Note 📌

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.

How each toolchain gets a spec to run

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.

Templates compiled by the dev server (AOT)Templates compiled in the browser (JIT)Type-checking
Dev serverat startupDev serverper fileBrowseron mountwebpackJITType-checkthe projectBundlethe specCompiletemplatesRun the testAnalog@angular/buildJITTypeScriptprogramServethe fileCompiletemplatesRun the testAnalog@angular/buildAOTTypeScriptprogramCompiletemplatesServethe fileRun the testAnalogfastCompileAOTScanthe projectCompiletemplatesRun the testSWCJITStrip typesCompiletemplatesRun the testOXCAOTStrip typesCompiletemplatesRun the test
How each toolchain gets a spec to run
ToolchainDev server, at startupDev server, per fileBrowser, on mount
webpack JITNothingType-check the project (whole project), Bundle the specCompile templates, Run the test
Analog, @angular/build JITTypeScript program (whole project)Serve the fileCompile templates, Run the test
Analog, @angular/build AOTTypeScript program (whole project), Compile templates (whole project)Serve the fileRun the test
Analog, fastCompile AOTScan the project (whole project)Compile templatesRun the test
SWC JITNothingStrip typesCompile templates, Run the test
OXC AOTNothingStrip types, Compile templatesRun 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:

Known issues per setup

One spec per known issue, run once per setup and never timed.

SetupModeoverrideComponentOXC #585OXC #586
webpackJITWorksWorksWorks
AnalogJITWorksWorksWorks
@angular/buildJITWorksWorksWorks
SWCJITWorksWorksWorks
AnalogAOTFailsWorksWorks
Analog fastAOTFailsWorksWorks
@angular/buildAOTFailsWorksWorks
OXCAOTWorksFailsFails

What each one means:

Some limits don’t show up in a test:

Total run time

From spawning Cypress to its exit. Switch to Stress, 1000 to see the compilers pull apart:

Total run time per setup

From starting Cypress to its exit. Median of 3 cold runs, fastest first; the percentage compares with webpack.

Startup, until the first test runsTest bodiesLoading the other specs, shutdown
0 s10 s20 s30 s40 sOXCAOT26.8 s · −22%SWCJIT27.2 s · −20%Analog fastAOT28.5 s · −17%AnalogJIT29.4 s · −14%@angular/buildJIT29.5 s · −14%AnalogAOT29.6 s · −13%@angular/buildAOT29.7 s · −13%webpackJIT34.1 s · baseline
Wall time per setup, App + stress, in seconds
SetupWall timeStartupTest bodiesCompared with webpack
OXC AOT26.8 s3.4 s4.7 s−22%
SWC JIT27.2 s3.4 s5.4 s−20%
Analog fast AOT28.5 s5.2 s4.7 s−17%
Analog JIT29.4 s5.8 s5.5 s−14%
@angular/build JIT29.5 s5.9 s5.5 s−14%
Analog AOT29.6 s6.3 s4.7 s−13%
@angular/build AOT29.7 s6.5 s4.7 s−13%
webpack JIT34.1 s7.3 s5.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:

Time to the first test, 300 vs 1000 components

Cypress boot, dev server start, dependency pre-bundling and compiling the first spec. Median of 3 cold runs on the stress specs.

300 components1000 components
0 s2 s4 s6 s8 s10 s12 sOXCAOT+0.9 sSWCJIT+1.3 sAnalog fastAOT+2.4 s@angular/buildJIT+2.1 sAnalogJIT+2.3 sAnalogAOT+2.7 s@angular/buildAOT+2.5 swebpackJIT+2.6 s
Time to the first test, in seconds
Setup300 components1000 components
OXC AOT3.5 s4.4 s
SWC JIT3.8 s5.2 s
Analog fast AOT5.3 s7.6 s
@angular/build JIT6.2 s8.3 s
Analog JIT6.0 s8.3 s
Analog AOT6.2 s8.9 s
@angular/build AOT6.6 s9.1 s
webpack JIT8.0 s10.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:

Time to the first test, by phase

Phases between Cypress's own events, median of 3 cold runs. The percentage compares with the fastest.

Cypress boot and browser launchDev server startFirst spec: compile and load
0 s5 s10 s15 sOXCAOT4.4 s · fastestSWCJIT5.2 s · +18%Analog fastAOT7.7 s · +74%@angular/buildJIT8.3 s · +89%AnalogJIT8.3 s · +90%AnalogAOT8.9 s · +104%@angular/buildAOT9.1 s · +108%webpackJIT10.6 s · +141%
Startup phases per setup, Stress, 1000, in seconds
SetupCypress bootDev server startBrowser launchFirst spec: compile and load
OXC AOT0.8 s1.9 s0.7 s1.0 s
SWC JIT0.8 s1.8 s0.7 s1.9 s
Analog fast AOT0.8 s5.2 s0.7 s1.0 s
@angular/build JIT0.8 s4.9 s0.7 s1.9 s
Analog JIT0.8 s5.0 s0.7 s1.9 s
Analog AOT0.8 s6.5 s0.7 s1.0 s
@angular/build AOT0.8 s6.7 s0.7 s1.0 s
webpack JIT0.8 s3.3 s0.7 s5.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 running the tests

Time spent inside the tests of the 3 stress specs. Median of 3 cold runs.

AOT: templates compiled by the dev serverJIT: templates compiled in the browser
0 s1 s2 s3 s0.9 s@angular/build1.0 sAnalogfast1.0 sAnalog1.0 sOXC2.4 s@angular/build2.4 sSWC2.4 sAnalog2.5 swebpackAOTJIT
Test bodies per setup, Stress, 1000, in seconds
SetupTest bodies
@angular/build AOT0.9 s
Analog fast AOT1.0 s
Analog AOT1.0 s
OXC AOT1.0 s
@angular/build JIT2.4 s
SWC JIT2.4 s
Analog JIT2.4 s
webpack JIT2.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 memory per setup

Peak PSS, sampled every second across the CLI, Electron and the dev server. Median of 3 cold runs.

0 GB1 GB2 GB3 GB4 GBAnalog fastAOT1.88 GBSWCJIT2.02 GBAnalogJIT2.25 GBOXCAOT2.29 GBAnalogAOT2.34 GB@angular/buildJIT2.41 GB@angular/buildAOT2.68 GBwebpackJIT3.43 GB
Peak memory per setup, Stress, 1000
SetupPeak PSS
Analog fast AOT1.88 GB
SWC JIT2.02 GB
Analog JIT2.25 GB
OXC AOT2.29 GB
Analog AOT2.34 GB
@angular/build JIT2.41 GB
@angular/build AOT2.68 GB
webpack JIT3.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.