Native AOT en .NET 10: publica un Minimal API que se salta el peaje del JIT
Cada vez que un servicio .NET orquestado en contenedores escala, paga el mismo peaje: el runtime arranca, el compilador JIT se calienta, y solo entonces la app empieza a atender tráfico. Multiplica eso por la cantidad de réplicas que tu autoscaler lance durante un pico de tráfico, y "unos cientos de milisegundos de arranque" se convierte en dinero real y en latencia real sobre tu P99. Native AOT existe para saltarse ese peaje del todo — sin JIT, sin calentamiento, solo código máquina nativo que empieza a correr de inmediato.
Este post es un cambio de ritmo deliberado. Los últimos cuatro artículos integraron Claude en .NET de una forma u otra; este es .NET puro — sin ningún LLM a la vista, solo un Minimal API que arranca rápido porque nunca le pidió permiso a un compilador JIT.
Qué es Native AOT en realidad
"AOT" significa compilación ahead-of-time (por adelantado) — lo opuesto a la compilación JIT ("just-in-time") que .NET hace normalmente. Con Native AOT, dotnet publish ejecuta un compilador nativo de IL que convierte toda tu app, más las piezas del runtime que realmente usa, en un único ejecutable nativo en el momento de publicar. No queda IL esperando a ser compilado por el JIT al arrancar, porque la app desplegada no tiene compilador JIT en absoluto.
De ahí se derivan tres consecuencias:
- El arranque es tan rápido como el código nativo, porque la CPU ejecuta código máquina desde la primera instrucción — no hay nada que compilar antes.
- El binario es autocontenido y recortado (trimmed) — solo incluye el código que el grafo de dependencias de tu app realmente alcanza, no el framework completo.
-
Algunos comportamientos dinámicos dejan de funcionar — cualquier cosa que dependa de generar o cargar código en tiempo de ejecución (serialización basada en reflexión,
Assembly.LoadFile, generación de código en runtime) ya no tiene nada contra qué generarse. Volvemos a esto más adelante; es lo que decide si AOT le queda bien a tu servicio.
Microsoft corre su propio benchmark comparando una app con Native AOT, una app recortada pero con JIT, y una app sin recortar, en tamaño en disco, memoria y tiempo de arranque — Native AOT gana en las tres en esa comparación. En vez de repetir números que no puedo verificar contra tu hardware y tu carga de trabajo, la sección "Números reales" más abajo te muestra exactamente cómo producir los tuyos.
Construyendo un servicio real: la API de pedidos de Aurora Coffee Co.
La misma tienda ficticia de los posts de Claude, esta vez sin Claude. Aurora Coffee Co. necesita una API de pedidos — pequeña, enfocada, y exactamente la forma de servicio que se despliega cien veces a lo largo de una flota. Genérala con la plantilla oficial lista para AOT:
dotnet new webapiaot -o AuroraCoffee.Orders
cd AuroraCoffee.Orders
webapiaot (Web API Native AOT) no es la plantilla de Web API normal con un flag activado — está diseñada distinta a propósito:
- Llama a
WebApplication.CreateSlimBuilder()en vez deCreateBuilder(), que registra solo las funciones esenciales de ASP.NET Core (sin HTTPS/HTTP3 en Kestrel, sin integración con IIS) para mantener pequeño el resultado recortado. - Solo Minimal APIs — MVC no es compatible con AOT, así que no hay controladores a los que recurrir.
- La serialización JSON está conectada a un
JsonSerializerContextgenerado por código fuente en vez de por reflexión, porque elSystem.Text.Jsonbasado en reflexión no funciona una vez que se recortaron los metadatos de tipo que necesita.
El dominio y los datos
Una decisión deliberada desde el inicio: sin Entity Framework Core. EF Core hoy no es compatible con AOT — su pipeline de consultas depende de generación de código en tiempo de ejecución que Native AOT no puede producir. Para un servicio real recurrirías a Dapper o ADO.NET puro; para este ejemplo, un almacén en memoria inyectable ilustra lo mismo sin necesitar una base de datos:
public sealed record Order(string Id, string Sku, int Quantity, string Status);
public sealed class OrdersStore
{
private readonly Dictionary<string, Order> _orders = new()
{
["A-2001"] = new("A-2001", "ETH-250", 2, "processing"),
["A-2002"] = new("A-2002", "COL-1KG", 1, "shipped"),
};
public Order? Find(string id) => _orders.GetValueOrDefault(id);
public Order Place(string sku, int quantity)
{
var order = new Order($"A-{Random.Shared.Next(3000, 9999)}", sku, quantity, "processing");
_orders[order.Id] = order;
return order;
}
}
Program.cs — las partes con forma de AOT
using System.Text.Json.Serialization;
var builder = WebApplication.CreateSlimBuilder(args);
builder.Services.AddSingleton<OrdersStore>();
builder.Services.ConfigureHttpJsonOptions(options =>
{
options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonContext.Default);
});
var app = builder.Build();
var orders = app.MapGroup("/orders");
orders.MapGet("/{id}", (string id, OrdersStore store) =>
store.Find(id) is { } order ? Results.Ok(order) : Results.NotFound());
orders.MapPost("/", (PlaceOrderRequest request, OrdersStore store) =>
{
var order = store.Place(request.Sku, request.Quantity);
return Results.Created($"/orders/{order.Id}", order);
});
app.Run();
public sealed record PlaceOrderRequest(string Sku, int Quantity);
[JsonSerializable(typeof(Order))]
[JsonSerializable(typeof(PlaceOrderRequest))]
internal partial class AppJsonContext : JsonSerializerContext;
Ese AppJsonContext es la parte que todo Minimal API con Native AOT necesita, y la que todo tutorial que se la salta te va a dejar depurando una excepción en tiempo de ejecución. Cada tipo que cruza el cuerpo HTTP — request o response — necesita una entrada [JsonSerializable] aquí. Si te olvidas de uno, en vez de un error de compilación obtienes un fallo en runtime la primera vez que ese tipo necesite serializarse, porque el respaldo basado en reflexión sencillamente ya no está.
Publicarlo es el mismo comando que cualquier app autocontenida, ya que la plantilla webapiaot ya puso <PublishAot>true</PublishAot> en el .csproj:
dotnet publish -r linux-x64 -c Release
Presta atención a la salida del build en este paso — cada advertencia de AOT o de recorte que imprime es un reporte de bug real, no ruido. Una app que se publica sin advertencias se comporta igual que la versión compilada con JIT; una que se publica con advertencias puede fallar en runtime en una ruta de código que tus pruebas no cubrieron. Corrígelas antes de desplegar, no después de una alerta de guardia.
Números reales: mídelo tú mismo
En vez de citar un benchmark corrido en un hardware que no es el tuyo, publica las tres variantes de la misma app y compáralas en el tuyo:
# 1. Dependiente del framework (necesita el runtime de .NET instalado en la máquina destino)
dotnet publish -c Release -o out/framework-dependent
# 2. Autocontenida + recortada, todavía compilada con JIT
dotnet publish -r linux-x64 -c Release --self-contained -p:PublishTrimmed=true -o out/trimmed
# 3. Native AOT
dotnet publish -r linux-x64 -c Release -o out/aot
Luego compara lo que realmente importa para un servicio desplegado:
-
Tamaño en disco:
du -sh out/*/AuroraCoffee.Orders*(o el tamaño de la imagen de contenedor, si empaquetas una) — esto es lo que descargas por red en cada despliegue. -
Tiempo de arranque: mide el hueco entre el inicio del proceso y la primera petición exitosa, por ejemplo
time curl --retry 20 --retry-connrefused http://localhost:5000/orders/A-2001justo después de lanzar cada variante. -
Memoria en reposo:
ps -o rss -p <pid>(Linux) o el working set del Administrador de Tareas (Windows), unos segundos después de arrancar y sin tráfico todavía.
El propio benchmark de plantilla de Microsoft (tamaño, memoria y tiempo de arranque comparados entre AOT, recortada y sin recortar) muestra a Native AOT por delante en las tres — mira la gráfica en la documentación de Native AOT en ASP.NET Core. Tus números van a variar según la carga de trabajo y el hardware, pero la dirección — AOT más pequeña y más rápida, sin recortar la más grande y lenta — debería mantenerse.
Qué es realmente nuevo en .NET 10 (no heredado de .NET 7/8)
Native AOT en sí llegó en .NET 7 y maduró a lo largo de .NET 8 y 9. .NET 10 agrega tres cosas que vale la pena conocer en concreto:
Metadatos de ensamblado IsAotCompatible. Los autores de librerías ahora pueden declarar compatibilidad con AOT de forma explícita:
<PropertyGroup>
<IsAotCompatible>true</IsAotCompatible>
</PropertyGroup>
Activarlo enciende automáticamente los analizadores de recorte, single-file y AOT para esa librería. Combínalo con VerifyReferenceAotCompatibility en tu propia app para que te avise cuando una dependencia no haya hecho esa promesa:
<PropertyGroup>
<IsAotCompatible>true</IsAotCompatible>
<VerifyReferenceAotCompatibility>true</VerifyReferenceAotCompatibility>
</PropertyGroup>
Las apps de un solo archivo apuntan a AOT por defecto. Un único archivo .cs ejecutado con dotnet run app.cs ahora se puede publicar directo a un ejecutable nativo con dotnet publish app.cs — sin necesitar archivo de proyecto. .NET 10 hace que AOT sea el valor por defecto para estas apps; puedes desactivarlo por archivo si un script necesita un paquete incompatible con AOT:
#:property PublishAot=false
Las apps de consola obtienen imágenes de contenedor nativas gratis. dotnet publish /t:PublishContainer ahora funciona en cualquier app de consola, no solo en proyectos de ASP.NET Core y Worker Service — ya no hace falta activar <EnableSdkContainerSupport>. Combínalo con Native AOT y un worker o una herramienta de CLI pasa directo de código fuente a una imagen de contenedor mínima en un solo comando.
Cuándo usarlo — y cuándo dejarlo en paz
Recurre a Native AOT cuando corres muchas instancias del mismo servicio — el tipo de carga de trabajo donde recortar el tiempo de arranque y la memoria de una réplica se paga solo cien veces a lo largo de una flota: microservicios en contenedores, funciones serverless, cualquier cosa que un autoscaler encienda y apague bajo carga.
Déjalo en paz cuando tu stack depende de las funciones dinámicas que no puede hacer: EF Core (hoy no compatible con AOT), librerías intensivas en reflexión que aún no agregaron generadores de código fuente, MVC, Blazor Server, SignalR completo, o Session — todas sin soporte o solo parcialmente soportadas bajo Native AOT a partir de .NET 10. Y recuerda que los binarios de AOT son específicos de plataforma: un publish para linux-x64 solo corre en linux-x64, así que el despliegue dependiente del framework y multiplataforma sigue siendo más simple si soportas muchas combinaciones de sistema operativo y arquitectura desde un mismo build.
Regla general: Native AOT cuando la cantidad de instancias es alta y tus dependencias son compatibles con AOT; quédate con JIT (opcionalmente recortado) cuando EF Core o librerías intensivas en reflexión sean pieza central. Y si el proveedor de una librería te dice que su paquete "funciona bien con reflexión", ese es el momento de recordar que Native AOT no hace favores — simplemente no ejecuta código que nunca estuvo ahí para empezar.
Puntos Clave
- AOT cambia dinamismo por velocidad — sin JIT significa sin paso de compilación en runtime, pero también sin reflexión sin límites, sin generación de código en runtime, sin carga dinámica de ensamblados. Sabe cuáles de esas cosas realmente necesita tu app antes de comprometerte.
-
CreateSlimBuilder+ JSON generado por código fuente es la forma de un Minimal API con AOT —dotnet new webapiaotgenera ambos correctamente; saltarse elJsonSerializerContextes la forma número uno de romper uno. - EF Core es el mayor bloqueador real — si tu servicio depende fuerte de EF, Native AOT todavía no es una ganancia directa; Dapper o ADO.NET son las alternativas compatibles con AOT hoy.
- Confía en las advertencias de publish, no en tu instinto — cero advertencias de AOT/recorte al publicar significa que el build de AOT se comporta como el build con JIT; cualquier advertencia es un posible fallo en runtime esperando la entrada correcta.
-
.NET 10 extiende AOT más allá de los servicios web — metadatos
IsAotCompatiblepara autores de librerías, apps de un solo archivo con AOT por defecto, e imágenes de contenedor nativas para cualquier app de consola son nuevos en esta versión, no el mismo "AOT 101" repetido por cuarto año consecutivo.




Top comments (0)