Wilson Vargas

CoreCLR en .NET 11: adiós Mono, así cambia tu app .NET MAUI

En .NET 11, .NET MAUI deja Mono y pasa a CoreCLR. Qué cambia en Android e iOS, qué se puede romper y cómo migrar y medir tu app paso a paso.

CoreCLR en .NET 11: Mono se reemplaza por CoreCLR

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

Dónde corre tu código .NET en .NET 10 y .NET 11

Con .NET 11 tu app móvil corre sobre el mismo runtime que tu backend. Eso significa:

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:

Línea de tiempo de CoreCLR en móviles
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:

  1. 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.
  2. 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.

Cómo se ejecuta tu código con CoreCLR en Android e iOS

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:

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:

Runtime async: máquina de estados vs runtime

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:

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:

Compartir
Wilson Vargas
Escrito por

Wilson Vargas

Programador .NET con 8 años de experiencia, apasionado por la tecnología. Con habilidades en el desarrollo de aplicaciones utilizando tecnologías de la plataforma .NET.

Posts relacionados

Comentarios