.NET 9 ReadyToRun publish for ARM64 took 35.03 seconds
In this article
Publishing for win-arm64 with .NET SDK 9.0.316 took 1.75 seconds for framework-dependent output and 35.03 seconds for self-contained output with ReadyToRun.
Self-contained publishing with ReadyToRun had a substantial build-time cost. The difference was 20x on this machine, yet execution time did not change, which was the unexpected part. Both out-jit and out-r2r reported 43 ms for the work measured inside the app. Looking at the whole process after warm-up, both settled at 0.12 seconds. The publish cost stood out, while the runtime benefit did not.
The measurement date was 2026-07-02, recorded in my notes as July 2, 2026. The machine was a Surface Pro 11th Edition with a Snapdragon X Elite X1E80100, running Windows 11 Pro 10.0.26200 ARM64. It was connected to AC power, with the default Balanced power plan unchanged. Changing these conditions could change how the publish time looks.
Two publish configurations
I used a small console application created with dotnet new console. After running a Release build once, I published the same project for win-arm64 in two ways. The normal output was framework-dependent; the comparison output was self-contained with ReadyToRun enabled.
dotnet publish -c Release -r win-arm64 --self-contained false -o out-jit
dotnet publish -c Release -r win-arm64 --self-contained true -p:PublishReadyToRun=true -o out-r2r
I measured execution by launching from each publish output directory. The first launch includes disk and runtime loading, so I kept all five runs and looked at the warmed values. Whether the first number is discarded changes the impression considerably, so all five are shown.
Measure-Command { .\out-jit\hello.exe }
Measure-Command { .\out-r2r\hello.exe }
This was not a program dedicated only to startup measurement. Its main body ran an integer loop and then printed the calculation result and elapsed time measured inside the app. That choice mattered later.
Publish time and output size
Here are the times for creation, restore, build, and publish, along with output sizes. out-jit was 152852 bytes across 5 files. out-r2r was 87784078 bytes across 189 files. With self-contained output, the size grew by 574.3x and the file count by 37.8x.
| Operation | Command | elapsed_s | Output size | File count |
|---|---|---|---|---|
| new | dotnet new console | 2.70 seconds | ||
| restore | dotnet restore | 1.61 seconds | ||
| build cold | dotnet build -c Release | 4.08 seconds | ||
| build warm | dotnet build -c Release | 1.31 seconds | ||
| publish jit | framework-dependent win-arm64 | 1.75 seconds | 152852 bytes | 5 files |
| publish r2r | self-contained + ReadyToRun win-arm64 | 35.03 seconds | 87784078 bytes | 189 files |
The cold build took 4.08 seconds and the warm build 1.31 seconds, a 3.1x difference. Restore alone took 1.61 seconds; the ordinary publish also included 110 ms of restore. On the ReadyToRun side, restore alone took 28.09 seconds, accounting for most of the 35.03 seconds.
Next, the execution times. All five runs are shown as measured.
| Output | Process execution time for all five runs | Last stdout |
|---|---|---|
| out-jit | 2.48 seconds / 0.13 seconds / 0.12 seconds / 0.12 seconds / 0.12 seconds | 119999995 43ms |
| out-r2r | 0.83 seconds / 0.12 seconds / 0.13 seconds / 0.12 seconds / 0.12 seconds | 119999995 43ms |
Looking only at the first launch, out-r2r's 0.83 seconds was faster than out-jit's 2.48 seconds. From the second launch onward, both settled around 0.12 seconds. It is difficult to call ReadyToRun the winner for everyday automation based on this alone.
What differed from my expectation
I expected ReadyToRun to make some difference at runtime. Since it reduces JIT work, I loosely expected process startup to become lighter. The name also sounds as if it should be faster.
The measured result was different: the in-app processing time was 43 ms for both out-jit and out-r2r. The warmed whole-process time also matched at 0.12 seconds. I spent 35.03 seconds publishing, but this measurement showed no runtime gain.
That does not mean ReadyToRun failed to work. It means the measurement was poorly designed.
The integer loop dominated this application. Once JIT compiled it once, subsequent runs only executed the native code. ReadyToRun should show its effect by avoiding JIT at startup, but I designed the test so that effect was buried under the main processing time. I wanted to observe startup differences and deliberately added a loop instead. In retrospect, that looks like a design error.
This is the largest failure marker in the test. To evaluate R2R, I should have repeatedly launched a short-lived process or measured a command that exits immediately after startup. The loop was somewhat misaligned with the use case I wanted to inspect.
The size cost was obvious
Runtime differences were not visible, but the output-size difference could not be hidden. Framework-dependent out-jit was 152852 bytes and 5 files. Self-contained + ReadyToRun out-r2r became 87784078 bytes and 189 files.
That directly affects distribution. Running one copy locally is not a problem even at 87,784,078 bytes, but an operation that keeps many small CLIs per task has to deal with more 189-file directories. Placing them under OneDrive or in Git makes them even less convenient. They are awkward both as sync targets and visually.
Self-contained output does have the benefit of bundling the runtime, so the target environment does not need .NET installed. But this machine already has .NET SDK 9.0.316, and I am only running my own automation, so that benefit is small. The situations that justify paying a 574.3x size increase are limited.
Build cache also mattered
The fall from 4.08 seconds for a cold build to 1.31 seconds for a warm build was significant to me. The difference was 2.77 seconds. Even for a small console application, the first and later builds felt different.
Restore alone took 1.61 seconds. The normal publish completed restore in 110 ms, while the ReadyToRun publish spent 28.09 seconds restoring. It is hard to separate from this JSON whether that was a one-time effect or related to fetching self-contained dependencies. The numbers are measured, but there is not enough evidence to assert the reason.
This is another part I would like to measure again before treating the article as settled. Still, it is clear that 35.03 seconds cannot be treated like 1.75 seconds. If I publish repeatedly during development, ReadyToRun is not something I should enable casually.
Next time, measure startup only
My usual tools are short-lived CLIs called by automation. They take a filename, perform a small conversion, and exit. For that use case, the relevant metric is the time from process launch until the first real work, not the integer-loop processing time.
This measurement did show a first-run difference: 0.83 seconds for out-r2r versus 2.48 seconds for out-jit. But it is too weak to attribute that difference solely to ReadyToRun. Cache state, disk, runtime loading, and first-run side effects are mixed together.
Next I will test a nearly empty CLI and a CLI that reads a small library separately. I want to compare publish time, output size, first startup, and warm startup. The conclusion from this run may be less "ReadyToRun is slow" and more "I measured ReadyToRun incorrectly."
Even so, my operating default changed slightly. I will use framework-dependent output for normal development and personal CLIs. I will try self-contained + ReadyToRun only after confirming that the destination has no .NET runtime and startup time genuinely matters. At the moment, I have not found a reason on this machine to pay for a 35.03-second publish and an 87784078-byte output every time.