Performance Native AOT

By default, a .NET application ships as IL, an intermediate format that the .NET runtime translates into instructions your CPU can execute when the app launches, using a Just-In-Time (JIT) compiler. It works well, and it is why the same assembly can run on different machines. It also means a portion of your startup time is spent compiling.

Native Ahead-of-Time (AOT) moves that translation to publish time. Your code is compiled into machine code for a specific platform before it ever reaches a user's device, and the parts of the runtime your app actually uses are packaged with it.

Default PublishingNative AOT Publishing
When code is compiledAt runtime, as the app executesAt publish time
What shipsIL plus the runtimeMachine code for a target platform
Startup workRuntime loads and compiles ILApp starts executing directly
Build outputOne build serves multiple architecturesOne build per target platform
Runtime code generationAvailableNot available

The practical summary: you trade some flexibility at build time for speed at launch time.

Platform Support

6.6 Brings Native AOT to Android, iOS, Linux, macOS, and Windows

WebAssembly continues to use its existing publishing model.

iOS deserves a note up front. Apple prohibits JIT compilation, so an iOS build has always been compiled ahead of time. Until now that was Mono's FullAOT, which is part of MonoVM.

That distinction is why 6.6 matters on iOS beyond the stopwatch. Microsoft is consolidating .NET on CoreCLR: as of .NET 11 Preview 6, .NET MAUI builds for Android, iOS, and Mac Catalyst run on CoreCLR alone. Native AOT is a different implementation of the same ahead-of-time idea, built on the runtime the rest of .NET is converging on. What 6.6 adds on iOS is that model, which is also why the iOS numbers below move less than the rest.

Startup time is the first thing a user experiences, and it is one of the hardest things to improve after an application is already written. Native AOT improves it without asking you to restructure your app, change your architecture, or give up XAML and data binding.

Watch the 6.6 Community Standup on Native AOT

Uno Platform engineers discuss how it works and what it means for your applications.

Benchmarks

Uno.Chefs Cuts Startup Roughly in Half on Four of Five Platforms

The Native AOT documentation publishes startup figures for Uno.Chefs, our full reference application, on .NET 10. Chefs is a complete application with navigation, media, charts, and authentication, so the numbers come from a real workload.

PlatformDefault RuntimeStartup, DefaultStartup, Native AOTChange
AndroidMonoVM895 ms348 ms61% faster
iOSMonoVM940 ms742 ms21% faster
LinuxCoreCLR870 ms350 ms60% faster
macOSCoreCLR1347 ms555 ms59% faster
WindowsCoreCLR1605 ms824 ms49% faster

Actual startup times vary with hardware, so read these as direction.

iOS gains the least, for the reason above, and the runtime column shows it. Mono has already compiled that build ahead of time, so Native AOT has far less compilation work left to remove. On the four platforms that do JIT by default, startup lands between 39% and 51% of the default figure.

A Smaller App Gains 3.5x on Desktop and 2.7x on Android

Chefs is a large app. To see whether the same gains show up at a more ordinary scale, we also measured Build Pulse, a modest CI dashboard: one list of 750 records, a detail page, a settings page, MVUX state, and region-based navigation. Median of 20 launches per configuration on .NET 10.

PlatformBuildFirst FrameInteractiveMemory
Desktop (Windows)Managed1045 ms2151 ms282 MB
Desktop (Windows)Native AOT300 ms546 ms212 MB
AndroidManaged1768 ms2979 ms400 MB
AndroidNative AOT654 ms1082 ms301 MB

Desktop launches were 3.5x faster to first frame and 3.9x faster to interactive; Android was 2.7x on both. Memory dropped about 25% on each platform.

The Gain More Than Doubles Between First Frame and Interactive

The two columns above measure different things, and the difference is the interesting part.

First frame is the first pixels on screen. Interactive is the point where data has loaded, layout is done, and the app responds to a tap.

On desktop, Native AOT improved first frame by 745 ms and interactive by 1605 ms. The gap widens because the work between those two markers, parsing XAML, resolving bindings, loading data, and running layout, is exactly the kind of managed code the JIT has to compile before it can run. A XAML application does a lot of that work in its first second, which is why the benefit here tends to be larger than it would be for a small console tool.

Launch Variance Drops from 206 ms to 66 ms

Across 20 desktop runs, Native AOT varied by 66 ms between fastest and slowest first frame. The managed build varied by 206 ms.

With no JIT warm-up in the picture, consecutive launches land nearly on top of each other. For demos, kiosks, and anything else where a slow launch is visible to an audience, that consistency is worth as much as the average.

Setup

Your Project Stays the Same; Only Publishing Changes

You keep the project you already have. Same XAML, same C#, same MVUX or MVVM code, same single-project structure. Native AOT applies when you publish, so your daily development loop, including Hot Reload, stays on the standard model.

1. Add One Property to Your Project File

Set PublishAot in your .csproj:

.csproj
<PropertyGroup>
  <PublishAot>true</PublishAot>
</PropertyGroup>

Putting it in the project file, and not on the command line, also turns on compatibility analysis during regular builds, so you see problems while you are working instead of at publish time.

2. Publish Per Platform and Scope the RID with -p:PublishRid

Native AOT produces machine code, so you publish per platform and architecture:

PowerShell
dotnet publish -f net10.0-desktop -c Release -p:PublishRid=win-x64

Pass the runtime identifier through -p:PublishRid instead of the global -r flag. In a multi-targeted Uno Platform project, a global -r leaks into the restore of the other target frameworks, where it will try to resolve a runtime pack that does not exist for that platform. Scoping it this way keeps each target framework resolving its own runtime.

Repeat for each platform you ship.

3. Fix the Trim Warnings, Then Exercise Every Flow

The build tells you when something in your app or its dependencies is not compatible. Trim and AOT warnings are work to do: they point at code that may be removed from the final binary. Then launch the app and exercise your main flows, especially anything that loads data or navigates dynamically.

Native AOT on Android emits an XA1040 warning. This is expected and does not indicate a broken publish.

Publish, review warnings, test, and repeat per target.

Trade-offs

Publishes Take Up to 12x Longer and Packages Usually Grow

Trade-offWhat It Means for You
Longer publish timesSubstantially longer. Compiling to machine code is real work.
One build per platformYou publish separately for each target platform and architecture.
No runtime code generationAnything that emits or compiles code while the app runs will not work.
Reflection has limitsCode discovered only at runtime may be trimmed away. Source-generated alternatives are the reliable path.
Larger packages, usuallySize typically grows. See the numbers below.

In the Uno.Chefs figures, the Native AOT publish is 17% larger on Android, 18% on Linux, 39% on Windows, and 41% on macOS. iOS is the exception at 12% smaller.

Publish time is the trade most teams feel first. In our Build Pulse measurements, a desktop publish went from 19 seconds to 234, and an Android publish from 197 seconds to 677, roughly 12x and 3.4x. That cost lands in your release pipeline and not in your day: debug builds stay on the managed model, so your inner development loop is unaffected.

The runtime code generation row lands hardest on iOS teams, for a reason specific to how they got there. MonoVM paired FullAOT with an IL interpreter, so code the AOT compiler could not handle had somewhere to fall back to. Native AOT has no interpreter and no equivalent. An iOS build that leans on that fallback needs a code change, not a publish setting.

MVUX and Source-Generated Serialization Survived Trimming Untouched

The documentation is explicit that an application may require changes to run under Native AOT, and that some dependency sets rule it out entirely. Build Pulse was the easy case, and it is worth understanding why.

It published and ran under Native AOT with no [Bindable] attributes, no [DynamicDependency] annotations, and no preservation hints anywhere in its source. MVUX source-generated bindable proxies, value converters, region-based navigation, record-passing between pages, and two-way bound state all survived trimming untouched.

The Condition

Those pieces survived because they are visible to the trimmer as ordinary compiled code: MVUX generates its bindings at compile time, and the app serialized its data through a source-generated JsonSerializerContext instead of reflection. An application that resolves types by name at runtime, or relies on reflection-based serialization, has more work ahead of it.

Uno Platform also preserves property references used by XAML binding expressions automatically during the build, so the bindings in your pages keep working without manual annotation.

Across a full pass over every screen and interaction in the app, the Native AOT build behaved identically to the managed one.

When to Use

Use Native AOT When Startup Is Visible; Stay on the Default While Iterating

Reach for Native AOT WhenStay on the Default Model When
Startup time is something users notice or complain aboutYour app already starts fast enough
You are shipping to mobile devices or kiosks where launch speed is visibleYou are early in development and iterating rapidly
Consistent launch time matters, such as demos or kiosk displaysYour publish process is already tight on time
Your dependency list is small and modernYou depend on libraries that generate code or lean heavily on reflection

Native AOT is an option you turn on when the trade favors your application.

Prerequisites

NDK r27, the Visual Studio C++ Workload, and Xcode

  • Android requires NDK r27 or later, in addition to the usual .NET for Android requirements.
  • Windows requires the Visual Studio C++ desktop workload, because Native AOT uses the platform linker and C++ static runtime libraries.
  • Apple platforms require the standard Xcode toolchain you already use for iOS and macOS builds.
  • Linux requires the native toolchain listed in the .NET prerequisites.

Full prerequisites, per-platform notes, and the current list of limitations are in the Native AOT documentation.

Get Started

Try It on Uno.Chefs, Then Measure Your Own App

  1. Try it on Uno.Chefs, a complete application already configured for Native AOT, and a safe place to see the publish flow end to end before touching your own project.

Questions, results from your own applications, or problems you run into are welcome on our Discord or GitHub.