¿Sabías que tu app .NET MAUI y tu API en ASP.NET Core corrían sobre motores distintos? Hasta .NET 10, todo lo que escribías para servidor o escritorio se ejecutaba en CoreCLR, pero tus apps de Android, iOS y Mac Catalyst seguían corriendo sobre Mono, el mismo runtime que nos acompaña desde los tiempos de Xamarin.
Con .NET 11 eso se acabó. CoreCLR pasa a ser el único runtime para .NET MAUI en móviles, y Mono deja de estar disponible para Android, iOS y Mac Catalyst.
Si llevas tiempo leyendo este blog sabes que empecé escribiendo sobre Xamarin.Forms allá por 2017, así que ver a Mono despedirse después de más de 15 años me genera un poco de nostalgia 🥲. Pero la verdad es que es una de las mejores noticias para los que hacemos apps móviles con .NET en mucho tiempo.
En este artículo te cuento qué es CoreCLR, qué cambia exactamente en tu app, qué se puede romper y qué debes revisar antes de migrar tu proyecto a .NET 11.
¿Qué es CoreCLR y por qué importa?
CoreCLR es el runtime de .NET que usan ASP.NET Core, las aplicaciones de consola, los servicios de Azure y las apps de escritorio en Windows. Es el que se encarga de administrar la memoria, el recolector de basura (GC), el sistema de tipos y la ejecución de tu código.
Mono, por su parte, nació como la implementación multiplataforma de .NET y fue el corazón de Xamarin. Cuando llegó .NET MAUI, Mono siguió siendo el runtime de los móviles, mientras que el resto del ecosistema ya corría sobre CoreCLR.
¿El resultado? Dos runtimes con comportamientos, herramientas de diagnóstico y optimizaciones diferentes. Algo que funcionaba perfecto en tu API podía comportarse distinto en tu app móvil.

Con .NET 11 tu app móvil corre sobre el mismo runtime que tu backend. Eso significa:
- Las mejoras de rendimiento que Microsoft hace al JIT y al GC para servidores ahora también llegan a tus apps móviles.
- Las mismas herramientas de diagnóstico (
dotnet-trace,dotnet-counters) funcionan en Android e iOS. - Menos sorpresas del tipo "en mi API funciona, pero en el celular no".
Ojo: Blazor WebAssembly sigue usando Mono por defecto en .NET 11. CoreCLR para WebAssembly existe como opción experimental, pero todavía no es el camino principal.
¿Cómo llegamos hasta aquí?
Esto no pasó de un día para otro. Microsoft lo fue preparando durante todo el ciclo de .NET 11:

| Versión | Fecha | Qué pasó |
|---|---|---|
| .NET 10 | Nov 2025 | CoreCLR en Android disponible como experimento con UseMonoRuntime=false. |
| .NET 11 Preview 1 | Feb 2026 | Runtime async activado en CoreCLR y primeros pasos de CoreCLR en WebAssembly. |
| .NET 11 Preview 4 | May 2026 | CoreCLR pasa a ser el runtime por defecto en Android, iOS y Mac Catalyst. |
| .NET 11 Preview 6 | Jul 2026 | Mono desaparece de MAUI. Usar UseMonoRuntime=true produce un error de compilación. |
| .NET 11 GA | Nov 2026 | Versión estable. |
¿Y cómo funciona CoreCLR en iOS si Apple no permite JIT?
Esta fue la primera pregunta que me hice. CoreCLR es famoso por su compilador JIT, que convierte el IL a código nativo mientras la app se ejecuta. Pero Apple no permite generar código ejecutable en tiempo de ejecución en iOS.
La solución de Microsoft combina dos piezas:
- ReadyToRun (R2R): tu código se precompila a código nativo al momento de compilar la app. En modo composite, se compilan todos los ensamblados juntos, lo que permite optimizaciones entre ellos.
- Un intérprete nuevo para CoreCLR: lo que no se pudo precompilar lo ejecuta un intérprete, sin generar código nativo en el dispositivo.
En Android no existe esa restricción, así que ahí el JIT sigue presente y además puede optimizar sobre la marcha los métodos que más se usan.

Esta es la configuración que usa .NET MAUI por defecto en .NET 11:
| Plataforma | Debug | Release |
|---|---|---|
| Android | CoreCLR + JIT | CoreCLR + ReadyToRun composite parcial + JIT |
| iOS | CoreCLR + ReadyToRun composite parcial + intérprete | CoreCLR + ReadyToRun composite completo + intérprete |
| Mac Catalyst | CoreCLR + ReadyToRun composite parcial + intérprete | CoreCLR + ReadyToRun composite completo + intérprete |
| Windows | CoreCLR + JIT | CoreCLR + JIT + ReadyToRun |
Lo mejor de todo es que el intérprete no necesita ninguna propiedad de MSBuild. Ya viene incluido en el runtime y se activa solo cuando hace falta.
Mono vs CoreCLR: lo que cambia en la práctica
| Mono (.NET 10) | CoreCLR (.NET 11) | |
|---|---|---|
| Android Release | Mono AOT | ReadyToRun + JIT por niveles |
| iOS Release | Mono Full AOT | ReadyToRun completo + intérprete |
| Recolector de basura | SGen | GC generacional de CoreCLR (el mismo del servidor) |
| Diagnóstico | Limitado | dotnet-trace y dotnet-counters |
| Arranque en iOS / Mac Catalyst | Referencia | Generalmente más rápido que Mono |
| Arranque y tamaño en Android | Referencia | Dentro de un 10% respecto a Mono |
| Hot Reload y depuración | Maduros | Funcionan, pero todavía con detalles pendientes |
Los números de Android son los que publicó el equipo de .NET MAUI. Algunas apps grandes han reportado regresiones de arranque o de tamaño del APK, así que no asumas nada: mide tu propia app (más abajo te dejo cómo).
Manos al código: migrando tu proyecto a .NET 11
1. Actualiza el .csproj
Lo primero es cambiar el Target Framework. Si en algún momento pusiste UseMonoRuntime en tu proyecto, bórralo: en .NET 11 da error de compilación.
<PropertyGroup>
<!-- Antes: net10.0-android;net10.0-ios;net10.0-maccatalyst -->
<TargetFrameworks>net11.0-android;net11.0-ios;net11.0-maccatalyst</TargetFrameworks>
<!-- ❌ Bórralo. En .NET 11 Mono ya no existe y esto rompe la compilación -->
<!-- <UseMonoRuntime>true</UseMonoRuntime> -->
</PropertyGroup>
2. Revisa tu versión mínima de Android
CoreCLR en .NET 11 no soporta Android API 23 o inferior, ni la arquitectura x86. Si tu proyecto viene de la plantilla de .NET MAUI, seguramente tiene 21.0 como versión mínima, así que súbela:
<PropertyGroup>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">24.0</SupportedOSPlatformVersion>
</PropertyGroup>
Además, si en tu proyecto defines los RuntimeIdentifiers a mano, quita android-x86. Según el anuncio de mayo, el soporte para arm32 también estaba en revisión, así que revisa las notas de la versión final si todavía publicas para dispositivos de 32 bits.
3. Verifica en qué runtime está corriendo tu app
Me gusta tener esta clase en mis proyectos para confirmar en qué runtime se está ejecutando la app. Es especialmente útil si vas a comparar la misma app entre .NET 10 y .NET 11:
using System.Runtime;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace MiApp.Diagnostics;
public static class RuntimeInfo
{
// Mono tiene un tipo interno llamado Mono.Runtime; CoreCLR no.
public static bool IsMono => Type.GetType("Mono.Runtime") is not null;
public static string Describe() =>
$"""
Runtime: {(IsMono ? "Mono" : "CoreCLR")}
Framework: {RuntimeInformation.FrameworkDescription}
Arquitectura: {RuntimeInformation.ProcessArchitecture}
¿Soporta código dinámico?: {RuntimeFeature.IsDynamicCodeSupported}
¿Lo compila con JIT?: {RuntimeFeature.IsDynamicCodeCompiled}
GC: {(GCSettings.IsServerGC ? "Server" : "Workstation")} / {GCSettings.LatencyMode}
""";
}
Y la usas desde cualquier página, por ejemplo en tu MainPage.xaml.cs:
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
var info = RuntimeInfo.Describe();
RuntimeLabel.Text = info;
System.Diagnostics.Debug.WriteLine(info);
}
}
En Android deberías ver CoreCLR con IsDynamicCodeCompiled = True, porque ahí sí hay JIT. En iOS, en cambio, IsDynamicCodeCompiled debería ser False, ya que el código que no se precompiló lo ejecuta el intérprete.
Si necesitas código diferente por plataforma para mostrar esta información, en este artículo te explico las tres formas de hacerlo en .NET MAUI.
4. Ajusta la compilación (solo si lo necesitas)
Los valores por defecto están bien para la mayoría de apps, pero tienes algunas perillas para ajustar:
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<!-- Android: precompila TODOS los métodos con ReadyToRun.
Puede mejorar el arranque, pero el APK/AAB crece. Mídelo. -->
<MauiEnableFullReadyToRun>true</MauiEnableFullReadyToRun>
<!-- Recorta también tu código y tus NuGet (por defecto solo recorta el framework) -->
<TrimMode>full</TrimMode>
</PropertyGroup>
Y si quieres ir al extremo, está NativeAOT, que compila todo a un binario nativo sin JIT ni intérprete:
<PropertyGroup>
<PublishAot>true</PublishAot>
</PropertyGroup>
| Estrategia | ¿Cuándo compila? | Arranque | Tamaño | Código dinámico |
|---|---|---|---|---|
| JIT de CoreCLR | En ejecución | Más lento | Menor | ✅ Completo |
| Intérprete de CoreCLR | En ejecución | Más lento | Menor | ✅ Completo |
| ReadyToRun | Al compilar | Rápido | Mayor | ✅ Vía JIT o intérprete |
| NativeAOT | Al compilar | El más rápido | El más pequeño | ❌ No soportado |
✅ Cuándo usar NativeAOT: cuando el arranque y el tamaño son críticos y todas tus dependencias son compatibles con trimming.
❌ Cuándo evitarlo: si usas librerías que dependen mucho de reflexión. Además, en Android todavía es experimental y solo aplica al usar dotnet publish.
Mide antes y después
Aquí es donde muchos se equivocan: actualizan a .NET 11 y "sienten" que la app va más rápida (o más lenta). Lo correcto es medir la misma app en .NET 10 y en .NET 11, en un dispositivo físico y en modo Release.
Tiempo de arranque en Android
Este script abre la app 10 veces desde cero y muestra el tiempo de arranque de cada intento:
PKG=com.companyname.miapp
ACTIVITY=$(adb shell cmd package resolve-activity --brief $PKG | tail -n 1 | tr -d '\r')
for i in $(seq 1 10); do
# -S detiene la app antes de abrirla (arranque en frío), -W espera a que termine
adb shell am start -S -W -n "$ACTIVITY" | grep TotalTime
done
Anota los resultados en una tabla como esta y compara:
| Métrica | .NET 10 (Mono) | .NET 11 (CoreCLR) |
|---|---|---|
| Arranque en frío (promedio) | ___ ms | ___ ms |
| Tamaño del APK/AAB | ___ MB | ___ MB |
| Tamaño del IPA | ___ MB | ___ MB |
Trazas con dotnet-trace
Esta es una de las cosas que más me gusta de CoreCLR en móviles: ahora puedes usar las mismas herramientas de diagnóstico que usas en el backend. En Android, con CoreCLR, el componente de diagnóstico ya viene integrado en el runtime.
Primero ejecutas tu app con el puerto de diagnóstico configurado (usa 10.0.2.2 en el emulador y 127.0.0.1 en un dispositivo físico):
dotnet build -t:Run -c Release -p:DiagnosticAddress=10.0.2.2 -p:DiagnosticPort=9000 -p:DiagnosticSuspend=false -p:DiagnosticListenMode=connect
Y en otra terminal recoges la traza:
dotnet-trace collect --dsrouter android-emu --format speedscope
adb reverse tcp:9000 tcp:9001
dotnet-trace collect --dsrouter android --format speedscope
El archivo .speedscope.json lo puedes abrir en speedscope.app para ver en qué se está yendo el tiempo de arranque de tu app.
¿Qué se puede romper?
Para la mayoría de apps el cambio debería ser transparente, pero hay algunos casos en los que tienes que prestar atención:
- Librerías que dependen de Mono: si alguna librería busca tipos internos de Mono con reflexión o usa las APIs de embedding de Mono, va a fallar. Las APIs de embedding no están soportadas en .NET 11.
- Reflexión y generación de código dinámico: revisa las librerías que generan código en tiempo de ejecución, especialmente si activas
TrimMode=fullo NativeAOT. - Bindings nativos e interop: si tienes bindings propios de Java/Kotlin o de Objective-C/Swift, pruébalos a fondo.
- Hot Reload y depuración: funcionan en Visual Studio y VS Code, pero Microsoft reconoce que todavía son jóvenes en estas plataformas. El Hot Reload de XAML seguía en progreso en el último anuncio.
- Datos guardados: el cambio de runtime no debería afectar tus datos, pero si también vienes migrando desde Xamarin, revisa cómo migrar SecureStorage sin perder datos.
Si encuentras un problema, repórtalo en GitHub con la etiqueta CoreCLR: dotnet/android o dotnet/macios.
Bonus: runtime async
Además del cambio de runtime, .NET 11 trae otra novedad que llega gratis a tus apps al correr sobre CoreCLR: runtime async.
Hasta ahora, cada vez que escribías un método async, el compilador de C# generaba por detrás una máquina de estados (un struct con un método MoveNext). Por eso tus stack traces se llenaban de nombres raros como <FetchDataAsync>d__0.MoveNext().
Con runtime async, el runtime es quien se encarga de suspender y reanudar los métodos asíncronos:

Lo mejor es que no tienes que cambiar nada en tu código. Tu async/await sigue siendo exactamente el mismo:
public async Task<string> FetchDataAsync(HttpClient client, string url)
{
var response = await client.GetAsync(url);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
Runtime async está activo por defecto en CoreCLR desde Preview 1, y en Preview 5 recibió mejoras importantes en la suspensión de métodos. En uno de los benchmarks de las notas de esa versión, un escenario pasó de 6357 ms a 457 ms. No esperes esa diferencia en toda tu app, pero sí menos asignaciones de memoria en código con mucho await.
Checklist antes de publicar tu app en .NET 11
✅ Cambia el Target Framework a net11.0-* y elimina UseMonoRuntime.
✅ Sube la versión mínima de Android a API 24 y quita android-x86.
✅ Compila y publica en Release, no solo en Debug.
✅ Mide el arranque en frío y en caliente en un dispositivo físico.
✅ Compara el tamaño del APK/AAB/IPA contra tu versión en .NET 10.
✅ Prueba los flujos completos: login, navegación, red, archivos y notificaciones push.
✅ Revisa las librerías de terceros que usen reflexión o código dinámico.
✅ Verifica que Hot Reload y la depuración funcionen en tu flujo diario.
Conclusión
La llegada de CoreCLR a .NET MAUI en .NET 11 es el final de una etapa: Mono, el runtime que hizo posible Xamarin, se despide de los móviles. A cambio, por fin tenemos un solo runtime para todo: backend, escritorio y móviles.
En este artículo vimos:
- Qué es CoreCLR y por qué reemplaza a Mono.
- Cómo funciona en iOS sin JIT, gracias a ReadyToRun y al nuevo intérprete.
- Qué cambiar en tu
.csprojy qué propiedades puedes ajustar. - Cómo medir el arranque y sacar trazas con
dotnet-trace. - Qué se puede romper y cómo prepararte.
Mi recomendación: no esperes a noviembre. Crea una rama, cambia a .NET 11 y mide tu app hoy. Es mucho mejor encontrar los problemas ahora, mientras todavía puedes reportarlos, que el día que tengas que publicar.
¿Ya probaste tu app con CoreCLR? Cuéntame en X (Twitter) cómo te fue. Nos leemos pronto 😊
Referencias:
