Native AOT trong .NET — Biên dịch trước, khởi động nhanh gấp 10 lần

Posted on: 4/26/2026 5:18:15 PM

Bạn deploy một ASP.NET Core API lên Kubernetes. Mỗi pod khởi động mất 300-500ms, tiêu thụ 150MB RAM, container image nặng 200MB. Khi auto-scale từ 0 pod, cold start khiến request đầu tiên timeout. Giải pháp? Native AOT — biên dịch .NET trực tiếp thành mã máy tại thời điểm publish, không cần JIT runtime. Kết quả: khởi động 5-30ms, RAM chỉ 30-60MB, container image 20-40MB.

5-30msThời gian khởi động (so với 300-500ms JIT)
30-60MBRAM sử dụng (so với 100-150MB JIT)
~20MBKích thước container image
80%Giảm thời gian cold start

1. Native AOT là gì?

Trong mô hình truyền thống, .NET biên dịch C# thành IL (Intermediate Language), sau đó JIT (Just-In-Time) compiler chuyển IL thành mã máy tại runtime. Native AOT (Ahead-of-Time) thay đổi hoàn toàn: IL được biên dịch thành mã máy tại thời điểm publish, tạo ra một executable độc lập — không cần .NET runtime, không cần JIT.

graph LR
    subgraph "JIT (Truyền thống)"
        A["C# Code"] --> B["IL (DLL)"]
        B --> C["JIT Compiler"]
        C --> D["Native Code"]
        E[".NET Runtime"] --> C
    end
    subgraph "Native AOT"
        F["C# Code"] --> G["IL (DLL)"]
        G --> H["AOT Compiler"]
        H --> I["Native Binary"]
    end
    style A fill:#f8f9fa,stroke:#e94560,color:#2c3e50
    style F fill:#f8f9fa,stroke:#e94560,color:#2c3e50
    style D fill:#e94560,stroke:#fff,color:#fff
    style I fill:#4CAF50,stroke:#fff,color:#fff
    style E fill:#2c3e50,stroke:#fff,color:#fff

JIT biên dịch tại runtime (cần .NET runtime), AOT biên dịch tại publish time (binary độc lập)

Tại sao tên gọi "Ahead-of-Time"?

Vì quá trình biên dịch xảy ra trước khi ứng dụng chạy (ahead of time), thay vì ngay lúc chạy (just in time). Kết quả là một file binary đơn lẻ, tương tự như Go hay Rust compile ra. Không còn phụ thuộc .NET SDK hay runtime trên máy đích.

2. Khi nào nên (và không nên) dùng Native AOT?

Kịch bảnNative AOTJIT truyền thống
Serverless / AWS Lambda / Azure FunctionsLý tưởng — cold start cực nhanhCold start chậm, tốn chi phí
Kubernetes scale-from-zeroPod sẵn sàng trong msPod cần vài trăm ms
CLI tools / Microservices nhẹBinary nhỏ, chạy ngayCần runtime trên máy đích
IoT / Edge computingRAM thấp, disk nhỏQuá nặng cho thiết bị giới hạn
MVC + Razor ViewsKhông hỗ trợLựa chọn duy nhất
Ứng dụng dùng nhiều ReflectionPhải refactor hoặc dùng source genHoạt động bình thường
Plugin system / dynamic loadingKhông khả thiHỗ trợ đầy đủ

3. Bắt đầu với Native AOT trong ASP.NET Core

3.1 Tạo project từ template

dotnet new webapiaot -n MyAotApi
cd MyAotApi

Template webapiaot tự động cấu hình sẵn: PublishAot=true, CreateSlimBuilder, JSON Source Generator. Đây là điểm khởi đầu tốt nhất.

3.2 Cấu trúc project file

<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <PublishAot>true</PublishAot>
    <InvariantGlobalization>true</InvariantGlobalization>
  </PropertyGroup>
</Project>

InvariantGlobalization giảm đáng kể kích thước binary

Khi bật InvariantGlobalization, .NET bỏ qua ICU data (~30MB) dùng cho locale-specific formatting. Nếu API của bạn không cần format ngày/tiền theo locale cụ thể, đây là optimization đáng giá.

3.3 Program.cs với CreateSlimBuilder

using System.Text.Json.Serialization;

var builder = WebApplication.CreateSlimBuilder(args);

builder.Services.ConfigureHttpJsonOptions(options =>
{
    options.SerializerOptions.TypeInfoResolverChain
        .Insert(0, AppJsonSerializerContext.Default);
});

var app = builder.Build();

var todosApi = app.MapGroup("/todos");

todosApi.MapGet("/", () => new[]
{
    new Todo(1, "Deploy Native AOT", false),
    new Todo(2, "Benchmark cold start", true)
});

todosApi.MapGet("/{id}", (int id) =>
    id switch
    {
        1 => Results.Ok(new Todo(1, "Deploy Native AOT", false)),
        2 => Results.Ok(new Todo(2, "Benchmark cold start", true)),
        _ => Results.NotFound()
    });

todosApi.MapPost("/", (Todo todo) =>
{
    return Results.Created($"/todos/{todo.Id}", todo);
});

app.Run();

// === Records và Source Generator ===
public record Todo(int Id, string Title, bool IsComplete);

[JsonSerializable(typeof(Todo))]
[JsonSerializable(typeof(Todo[]))]
internal partial class AppJsonSerializerContext : JsonSerializerContext { }

Mọi type truyền qua HTTP body PHẢI có trong JsonSerializerContext

Đây là quy tắc bắt buộc với Native AOT. System.Text.Json không thể dùng reflection trong AOT — phải dùng source generator để tạo serialization code tại compile time. Quên khai báo type → runtime exception.

4. CreateSlimBuilder vs CreateBuilder

CreateSlimBuilder() là phiên bản "cắt gọn" của CreateBuilder(), chỉ giữ lại những tính năng thiết yếu nhất. Sự khác biệt:

graph TD
    A["CreateBuilder()"] --> B["Full Features"]
    B --> B1["IIS Integration"]
    B --> B2["HTTPS / HTTP3"]
    B --> B3["EventLog / Debug / EventSource logging"]
    B --> B4["Hosting Startup Assemblies"]
    B --> B5["UseStartup<T>()"]
    B --> B6["Static Web Assets"]
    B --> B7["Regex Route Constraints"]

    C["CreateSlimBuilder()"] --> D["Minimal Features"]
    D --> D1["appsettings.json config"]
    D --> D2["User Secrets"]
    D --> D3["Console logging"]
    D --> D4["Environment variables"]

    style A fill:#2c3e50,stroke:#fff,color:#fff
    style C fill:#4CAF50,stroke:#fff,color:#fff
    style B fill:#f8f9fa,stroke:#e94560,color:#2c3e50
    style D fill:#f8f9fa,stroke:#4CAF50,color:#2c3e50

CreateSlimBuilder loại bỏ các tính năng không cần thiết cho AOT, giảm kích thước binary

Tính năngCreateBuilderCreateSlimBuilder
appsettings.json
Console logging
User secrets
HTTPS / Kestrel HTTPS❌ (thêm thủ công được)
HTTP/3 (QUIC)❌ (thêm thủ công được)
IIS Integration
EventLog / Debug logging
UseStartup<T>
Static Web Assets
Regex route constraints

HTTPS không cần trong container production

Trong Kubernetes/cloud, TLS termination thường do ingress controller (nginx, Traefik, Cloudflare Tunnel) xử lý. Container chỉ cần lắng nghe HTTP nội bộ. Đây là lý do CreateSlimBuilder mặc định không bật HTTPS — đúng pattern cloud-native.

5. Compatibility Matrix — Cái gì hoạt động, cái gì không?

FeatureHỗ trợ đầy đủHỗ trợ một phầnKhông hỗ trợ
Minimal APIs
gRPC
JWT Authentication
CORS, HealthChecks
Rate Limiting, Output Caching
Response Compression
WebSockets, Static Files
SignalR
MVC / Razor Views
Blazor Server
Session
SPA hosting

6. JSON Source Generator — Chìa khóa cho AOT

Đây là thay đổi quan trọng nhất khi chuyển sang Native AOT. Bình thường, System.Text.Json dùng reflection để discover properties của object tại runtime. Native AOT không hỗ trợ reflection → phải dùng Source Generator để tạo serialization code tại compile time.

// Khai báo tất cả types cần serialize/deserialize
[JsonSerializable(typeof(Product))]
[JsonSerializable(typeof(Product[]))]
[JsonSerializable(typeof(OrderRequest))]
[JsonSerializable(typeof(OrderResponse))]
[JsonSerializable(typeof(ErrorResponse))]
[JsonSerializable(typeof(PaginatedResult<Product>))]
internal partial class AppJsonSerializerContext : JsonSerializerContext { }

// Đăng ký trong DI
builder.Services.ConfigureHttpJsonOptions(options =>
{
    options.SerializerOptions.TypeInfoResolverChain
        .Insert(0, AppJsonSerializerContext.Default);
});

// Models
public record Product(int Id, string Name, decimal Price, string Category);
public record OrderRequest(int ProductId, int Quantity);
public record OrderResponse(int OrderId, string Status, DateTime CreatedAt);
public record ErrorResponse(string Code, string Message);
public record PaginatedResult<T>(T[] Items, int Total, int Page, int PageSize);

Nguyên tắc: Mỗi type xuất hiện trong HTTP body → phải có [JsonSerializable]

Gồm cả generic types. Nếu bạn trả về PaginatedResult<Product>, cần khai báo cả [JsonSerializable(typeof(PaginatedResult<Product>))]. Compiler sẽ cảnh báo nếu thiếu — hãy đọc kỹ AOT warnings khi publish.

7. Publish và Deploy

7.1 Publish cho Linux x64

# Publish Native AOT cho Linux
dotnet publish -c Release -r linux-x64

# Kết quả: bin/Release/net10.0/linux-x64/publish/MyAotApi
# File size: ~15-25MB (so với ~150MB+ với runtime)
# Không cần .NET runtime trên máy đích

7.2 Dockerfile tối ưu

# Stage 1: Build
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src

COPY *.csproj .
RUN dotnet restore

COPY . .
RUN dotnet publish -c Release -r linux-x64 -o /app/publish

# Stage 2: Runtime — dùng distroless, không cần .NET runtime!
FROM mcr.microsoft.com/dotnet/runtime-deps:10.0-noble-chiseled
WORKDIR /app
COPY --from=build /app/publish .

USER $APP_UID
EXPOSE 8080
ENTRYPOINT ["./MyAotApi"]

runtime-deps thay vì aspnet image

Vì Native AOT binary đã tự chứa mọi thứ cần thiết, bạn chỉ cần base image với native dependencies (libc, openssl...) — đó là runtime-deps. Phiên bản chiseled (Ubuntu) càng nhỏ hơn: không có shell, không có package manager. Container image cuối cùng chỉ ~20MB.

7.3 So sánh kích thước container

ApproachBase ImageKích thước cuối
JIT + aspnet imagemcr.microsoft.com/dotnet/aspnet:10.0~220MB
JIT + trimmedmcr.microsoft.com/dotnet/aspnet:10.0~130MB
Native AOT + runtime-depsmcr.microsoft.com/dotnet/runtime-deps:10.0~50MB
Native AOT + chiseledruntime-deps:10.0-noble-chiseled~20MB

8. Xử lý các hạn chế thực tế

8.1 Entity Framework Core

EF Core dùng nhiều reflection nội bộ. Từ .NET 8+, EF Core đã cải thiện đáng kể AOT compatibility, nhưng vẫn cần cấu hình source generation cho model:

// Compiled Model — pre-generate EF metadata
// Chạy: dotnet ef dbcontext optimize
// Tạo ra CompiledModels/ folder chứa metadata đã compile

builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(connectionString)
           .UseModel(AppDbContextModel.Instance)); // Compiled model

8.2 Thư viện bên thứ ba

Không phải NuGet package nào cũng tương thích AOT. Kiểm tra bằng cách:

# Publish và đọc warnings
dotnet publish -c Release -r linux-x64 2>&1 | grep -i "trim\|aot"

# Mỗi warning = một vấn đề tiềm ẩn
# IL2026: Members annotated with 'RequiresUnreferencedCodeAttribute'
# IL3050: Using member with 'RequiresDynamicCodeAttribute'

Quy tắc vàng: Không có AOT warning = app hoạt động giống JIT

Microsoft cam kết: nếu dotnet publish không phát ra AOT/trimming warning nào, ứng dụng AOT sẽ hoạt động giống hệt JIT. Nhưng nếu có warning mà bạn bỏ qua — mọi thứ đều có thể xảy ra. Hãy fix hoặc suppress có chủ đích.

9. Benchmark thực tế

graph LR
    subgraph "Startup Time"
        A["JIT: 350ms"] ---|"giảm 93%"| B["AOT: 25ms"]
    end
    subgraph "Memory (idle)"
        C["JIT: 120MB"] ---|"giảm 65%"| D["AOT: 42MB"]
    end
    subgraph "Container Size"
        E["JIT: 210MB"] ---|"giảm 90%"| F["AOT: 22MB"]
    end
    style B fill:#4CAF50,stroke:#fff,color:#fff
    style D fill:#4CAF50,stroke:#fff,color:#fff
    style F fill:#4CAF50,stroke:#fff,color:#fff
    style A fill:#e94560,stroke:#fff,color:#fff
    style C fill:#e94560,stroke:#fff,color:#fff
    style E fill:#e94560,stroke:#fff,color:#fff

So sánh benchmark giữa JIT và Native AOT trên cùng một Minimal API (Todo CRUD)

Throughput thì sao?

Ở steady-state (sau khi JIT đã warm up), throughput giữa JIT và AOT gần tương đương — thậm chí JIT có thể nhỉnh hơn nhờ Profile-Guided Optimization (PGO) tại runtime. Native AOT thắng ở startup và footprint, không phải ở throughput. Nếu app của bạn chạy long-running và throughput quan trọng hơn startup, hãy cân nhắc kỹ.

10. Quy trình chuyển đổi từ JIT sang AOT

Bước 1: Thêm PublishAot vào .csproj
Bật <PublishAot>true</PublishAot>. Từ đây, IDE sẽ hiện AOT warnings khi build. Chưa cần publish, chỉ cần đọc warnings.
Bước 2: Thay CreateBuilder bằng CreateSlimBuilder
Loại bỏ các tính năng không cần thiết. Nếu cần HTTPS, thêm lại bằng UseKestrelHttpsConfiguration().
Bước 3: Thêm JSON Source Generator
Khai báo JsonSerializerContext với [JsonSerializable] cho mọi type trong HTTP body. Đây thường là bước tốn công nhất.
Bước 4: Fix AOT/Trimming warnings
Chạy dotnet publish và xử lý từng warning. Thay reflection bằng source generator hoặc pattern khác.
Bước 5: Test toàn diện
Chạy integration tests trên binary AOT. Đặc biệt test các edge case liên quan serialization, DI resolution, và middleware.
Bước 6: Benchmark và deploy
So sánh startup time, memory, throughput giữa JIT và AOT. Deploy AOT binary trong Dockerfile multi-stage với chiseled base image.

Kết luận

Native AOT không phải "silver bullet" cho mọi ứng dụng .NET. Nó có trade-off rõ ràng: build chậm hơn (60s+ so với 5s), không hỗ trợ MVC/Blazor Server, và đòi hỏi kỷ luật với source generators. Nhưng cho đúng use case — serverless functions, microservices scale-from-zero, CLI tools, edge computing — đó là game changer: khởi động nhanh gấp 10 lần, nhẹ gấp 3-5 lần, không phụ thuộc runtime.

Hãy bắt đầu với template webapiaot, fix hết warnings, benchmark so sánh. Native AOT trong .NET 10 đã production-ready cho Minimal APIs và gRPC — hai pattern phổ biến nhất trong kiến trúc microservices hiện đại.

Tham khảo