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
Table of contents
- 1. Native AOT là gì?
- 2. Khi nào nên (và không nên) dùng Native AOT?
- 3. Bắt đầu với Native AOT trong ASP.NET Core
- 4. CreateSlimBuilder vs CreateBuilder
- 5. Compatibility Matrix — Cái gì hoạt động, cái gì không?
- 6. JSON Source Generator — Chìa khóa cho AOT
- 7. Publish và Deploy
- 8. Xử lý các hạn chế thực tế
- 9. Benchmark thực tế
- 10. Quy trình chuyển đổi từ JIT sang AOT
- Kết luận
- Tham khảo
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.
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ản | Native AOT | JIT truyền thống |
|---|---|---|
| Serverless / AWS Lambda / Azure Functions | Lý tưởng — cold start cực nhanh | Cold start chậm, tốn chi phí |
| Kubernetes scale-from-zero | Pod sẵn sàng trong ms | Pod cần vài trăm ms |
| CLI tools / Microservices nhẹ | Binary nhỏ, chạy ngay | Cần runtime trên máy đích |
| IoT / Edge computing | RAM thấp, disk nhỏ | Quá nặng cho thiết bị giới hạn |
| MVC + Razor Views | Không hỗ trợ | Lựa chọn duy nhất |
| Ứng dụng dùng nhiều Reflection | Phải refactor hoặc dùng source gen | Hoạt động bình thường |
| Plugin system / dynamic loading | Không khả thi | Hỗ 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ăng | CreateBuilder | CreateSlimBuilder |
|---|---|---|
| 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?
| Feature | Hỗ trợ đầy đủ | Hỗ trợ một phần | Khô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
| Approach | Base Image | Kích thước cuối |
|---|---|---|
| JIT + aspnet image | mcr.microsoft.com/dotnet/aspnet:10.0 | ~220MB |
| JIT + trimmed | mcr.microsoft.com/dotnet/aspnet:10.0 | ~130MB |
| Native AOT + runtime-deps | mcr.microsoft.com/dotnet/runtime-deps:10.0 | ~50MB |
| Native AOT + chiseled | runtime-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
<PublishAot>true</PublishAot>. Từ đây, IDE sẽ hiện AOT warnings khi build. Chưa cần publish, chỉ cần đọc warnings.UseKestrelHttpsConfiguration().JsonSerializerContext với [JsonSerializable] cho mọi type trong HTTP body. Đây thường là bước tốn công nhất.dotnet publish và xử lý từng warning. Thay reflection bằng source generator hoặc pattern khác.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
Amazon Aurora DSQL — Distributed SQL Serverless cho kiến trúc Multi-Region
A2A Protocol v1.2 — Giao thức chuẩn để các AI Agent giao tiếp với nhau
Disclaimer: The opinions expressed in this blog are solely my own and do not reflect the views or opinions of my employer or any affiliated organizations. The content provided is for informational and educational purposes only and should not be taken as professional advice. While I strive to provide accurate and up-to-date information, I make no warranties or guarantees about the completeness, reliability, or accuracy of the content. Readers are encouraged to verify the information and seek independent advice as needed. I disclaim any liability for decisions or actions taken based on the content of this blog.