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 Publishing | Native AOT Publishing | |
|---|---|---|
| When code is compiled | At runtime, as the app executes | At publish time |
| What ships | IL plus the runtime | Machine code for a target platform |
| Startup work | Runtime loads and compiles IL | App starts executing directly |
| Build output | One build serves multiple architectures | One build per target platform |
| Runtime code generation | Available | Not available |
The practical summary: you trade some flexibility at build time for speed at launch time.
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.
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.
| Platform | Default Runtime | Startup, Default | Startup, Native AOT | Change |
|---|---|---|---|---|
| Android | MonoVM | 895 ms | 348 ms | 61% faster |
| iOS | MonoVM | 940 ms | 742 ms | 21% faster |
| Linux | CoreCLR | 870 ms | 350 ms | 60% faster |
| macOS | CoreCLR | 1347 ms | 555 ms | 59% faster |
| Windows | CoreCLR | 1605 ms | 824 ms | 49% 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.
| Platform | Build | First Frame | Interactive | Memory |
|---|---|---|---|---|
| Desktop (Windows) | Managed | 1045 ms | 2151 ms | 282 MB |
| Desktop (Windows) | Native AOT | 300 ms | 546 ms | 212 MB |
| Android | Managed | 1768 ms | 2979 ms | 400 MB |
| Android | Native AOT | 654 ms | 1082 ms | 301 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.
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:
<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:
dotnet publish -f net10.0-desktop -c Release -p:PublishRid=win-x64Pass 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.
Publishes Take Up to 12x Longer and Packages Usually Grow
| Trade-off | What It Means for You |
|---|---|
| Longer publish times | Substantially longer. Compiling to machine code is real work. |
| One build per platform | You publish separately for each target platform and architecture. |
| No runtime code generation | Anything that emits or compiles code while the app runs will not work. |
| Reflection has limits | Code discovered only at runtime may be trimmed away. Source-generated alternatives are the reliable path. |
| Larger packages, usually | Size 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.
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.
Use Native AOT When Startup Is Visible; Stay on the Default While Iterating
| Reach for Native AOT When | Stay on the Default Model When |
|---|---|
| Startup time is something users notice or complain about | Your app already starts fast enough |
| You are shipping to mobile devices or kiosks where launch speed is visible | You are early in development and iterating rapidly |
| Consistent launch time matters, such as demos or kiosk displays | Your publish process is already tight on time |
| Your dependency list is small and modern | You 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.
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.
Try It on Uno.Chefs, Then Measure Your Own App
- 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.
