Optymalizacja rozmiaru aplikacji .NET to jeden z kluczowych tematów współczesnego rozwoju oprogramowania, zwłaszcza w kontekście wdrożeń chmurowych, kontenerowych i środowisk o ograniczonych zasobach.
Mechanizmy single-file publishing i application trimming dostępne od .NET Core 3.0 zrewolucjonizowały tworzenie kompaktowych, samowystarczalnych aplikacji. Dzięki nowym narzędziom i technikom, jak PublishSingleFile, PublishTrimmed oraz Native AOT (ahead-of-time compilation), możliwa jest redukcja rozmiaru aplikacji nawet o 89% względem tradycyjnych wdrożeń. Prawidłowe wprowadzenie tych rozwiązań wymaga znajomości kompromisów oraz najlepszych praktyk, by efektywnie łączyć minimalny rozmiar, szybki czas uruchamiania i kompatybilność funkcji dynamicznych.
Wprowadzenie do optymalizacji aplikacji .NET
Optymalizacja rozmiaru aplikacji .NET oznacza fundamentalną zmianę strategii wdrożeń. W klasycznym .NET Framework obecność środowiska na maszynie docelowej pozwalała ograniczać pakiet dystrybucyjny. W .NET Core każde wdrożenie może zawierać pełne środowisko uruchomieniowe, co zwiększa rozmiar, ale zwiększa niezależność od systemu.
Jest to szczególnie istotne w:
- mikrousługach,
- kontenerach,
- aplikacjach chmurowych,
- środowiskach o niskich zasobach.
Wprowadzenie analizy statycznej i technik tree shaking/trimming pozwala wyeliminować zbędny kod jeszcze na etapie kompilacji.
Najważniejsze poziomy optymalizacji to:
- assembly-level trimming – usuwanie całych nieużywanych bibliotek,
- member-level trimming – eliminowanie nieużywanych typów, metod i właściwości.
Techniki te ułatwia także mechanizm Native AOT, który kompiluje aplikację do pojedynczego natywnego pliku bez zależności od runtime .NET, przyspieszając start i minimalizując rozmiar wdrożenia.
Single-file publishing – podstawy i implementacja
Single-file publishing upraszcza dystrybucję, zamykając całość aplikacji oraz jej zależności w jednym pliku wykonywalnym. Konfigurację wdrożenia opieramy o:
- PublishSingleFile – aktywuje pojedynczy plik,
- RuntimeIdentifier – determinuje platformę docelową,
- opcjonalnie SelfContained, IncludeNativeLibrariesForSelfExtract czy EnableCompressionInSingleFile.
Domyślny rozmiar prostej aplikacji konsolowej to około 68–84 MB, co pokazuje potrzebę dalszej optymalizacji (np. poprzez trimming).
Plik debugowania PDB nie jest domyślnie osadzany, można to zmienić ustawiając DebugType=”embedded” w projekcie.
Konfiguracja i parametry publikacji
Skuteczna publikacja wymaga poprawnych parametrów:
dotnet publishz-r(runtime),-c(konfiguracja),- flagi
--self-contained,-p:PublishSingleFile=true, - preferowane ustawienia w pliku projektu dla elastyczności CI/CD.
Warunkowe konfiguracje MSBuild umożliwiają indywidualne profile pod typy środowisk, ułatwiając optymalizację wdrożeń.
Application trimming – mechanizmy redukcji rozmiaru
Najbardziej efektywnie zmniejszymy rozmiar korzystając z trimmingu. Narzędzie ILLink służy do usuwania nieużywanego kodu na dwóch poziomach:
- Assembly-level trimming – usuwa nieużywane biblioteki (optymalny kompromis),
- Member-level trimming – eliminuje nieużywane klasy/metody/właściwości (większa redukcja, ryzyko błędów dynamicznych).
- Assembly-level trimming – aplikacja “Hello World”: z 68 MB do 26 MB (redukcja 62%), bezpiecznie dla większości projektów;
- Member-level trimming – “Hello World” nawet do 5,9 MB (89% redukcji), wyższe ryzyko wymaga testów przy użyciu refleksji.
Trymowanie aktywujemy przez PublishTrimmed=true. Zaawansowane opcje dostępne są przez TrimMode, a refleksję chronią atrybuty DynamicallyAccessedMembers lub RequiresUnreferencedCode.
Assembly-level trimming
Assembly-level trimming jest najbezpieczniejszym podejściem. Pozwala istotnie obniżyć rozmiar bez zagrożenia dla kodu korzystającego z refleksji.
Member-level trimming
Member-level trimming osiąga najmniejszy rozmiar, ale wymaga analizy dynamicznych wywołań i testów zgodności.
To tryb przeznaczony dla zaawansowanych projektów, gdzie każda optymalizacja rozmiaru jest krytyczna.
Zaawansowane techniki optymalizacji rozmiaru
Gdy trimming i single-file publishing nie wystarczą, sięgamy po:
- Native AOT – pełna natywna kompilacja,
- ReadyToRun – prekompilowane obrazy dla szybkiego startu,
- Zewnętrzne narzędzia (np. dotnet-warp) – dodatkowa kompresja.
Native AOT pozwala uzyskać bardzo małe aplikacje (np. ASP.NET Core Web API – 9 MB po AOT) oraz praktycznie natywną szybkość startu.
ReadyToRun pozwala połączyć trimming i szybki start, przy zachowaniu elastyczności środowiska .NET.
Native AOT – kompilacja ahead-of-time
Native AOT zapewnia pełną samodzielność aplikacji (bez zależności od runtime .NET) oraz najwyższą wydajność startu.
- Mały rozmiar pliku, szybki start, niskie zużycie RAM,
- Ograniczona obsługa refleksji, wyższe wymagania buildowania (Linux: clang, Windows: Visual C++),
- Wymaga ustawienia PublishAot=true.
ReadyToRun i optymalizacja uruchamiania
ReadyToRun to kompromis – prekompilacja krytycznych ścieżek przy zachowaniu elastyczności i możliwości korzystania z dynamicznych funkcji .NET.
- Szybszy start (30–50% szybciej),
- Nieco większy rozmiar aplikacji niż w przypadku trimming + single-file,
- Łatwość wdrożenia poprzez PublishReadyToRun=true.
Analiza wydajności i kompromisy
Decyzja o wykorzystaniu danej techniki optymalizacji wiąże się z kompromisami pomiędzy rozmiarem, czasem uruchamiania, zużyciem pamięci, kompatybilnością refleksji i trudnością kompilacji.
Podsumowanie głównych podejść:
| Technika | Typ optymalizacji | Typowy rozmiar | Kompatybilność dynamicznych funkcji | Czas startu |
|---|---|---|---|---|
| Single-file publishing | Pojedynczy plik | 65–84 MB | pełna | średni |
| Assembly-level trimming | Usuwanie bibliotek | 13–26 MB | wysoka | średni |
| Member-level trimming | Usuwanie składników | 5–9 MB | ograniczona | średni |
| Native AOT | Kompilacja natywna | ok. 9 MB | niska | bardzo szybki |
Metryki wydajności w praktyce
Doświadczenia praktyczne wskazują na następujące wyniki:
- ReadyToRun – start nawet 30–50% szybciej,
- Native AOT – cold start na poziomie 10–20 ms,
- Trimming – redukcja zużycia RAM nawet o 40%, Native AOT – ponad 60%.
Najlepsze praktyki implementacji
Proces optymalizacji wdrażaj stopniowo: zacznij od single-file, rozważ trimming, a na końcu – Native AOT. Każda zmiana powinna być poparta szerokim testowaniem.
Należy zarządzać konfiguracją przez plik projektu i MSBuild – warunki umożliwiają wiele profili optymalizacyjnych.
Typowe ustawienia MSBuild dla optymalizacji
Poniżej najczęstsze opcje w sekcji <PropertyGroup> projektu:
- TargetFramework – docelowa wersja .NET,
- RuntimeIdentifier – system docelowy,
- PublishSingleFile, SelfContained – tryby wydania,
- PublishTrimmed, TrimMode – stopień trymowania,
- PublishReadyToRun, ILLinkTreatWarningsAsErrors – dodatkowe opcje zwiększające bezpieczeństwo i wydajność.
Strategia testowania i walidacji
W przypadku optymalizacji testowanie powinno objąć:
- kod korzystający z refleksji,
- porównania wersji przed/po optymalizacji,
- benchmarki czasu i zużycia pamięci,
- testy z bibliotekami zależnymi (np. ORM, AutoMapper),
- automatyczną detekcję regresji wydajności.
Rozwiązywanie problemów i ostrzeżenia
Analizuj ostrzeżenia generowane przez trimmer – to klucz do poprawnego wdrożenia.
Najczęściej spotkasz ostrzeżenia:
- IL2026 – metody refleksyjne niekompatybilne z trimmingiem,
- IL2070 – podejrzane użycia typów dynamicznych.
Szczególną uwagę zwracaj na biblioteki dynamiczne, serializatory, dependency injection oraz ORM.
Obsługa ostrzeżeń trimmera
- RequiresUnreferencedCode – oznacza metody krytycznie uzależnione od intact kodu,
- UnconditionalSuppressMessage – pozwala świadomie tłumić wybrane ostrzeżenia z dokumentacją powodu,
- DynamicallyAccessedMembers – określa, które elementy typu muszą pozostać mimo trymowania.
Debugowanie i monitoring
Konfiguruj symbole i wykorzystuj narzędzia analityczne (ILSpy, APM) dostosowane do aplikacji zoptymalizowanych (Native AOT, trimmed .NET).
- DebugType=embedded – debugowanie dla single-file,
- Monitoring wydajności – narzędzia zgodne z Native AOT i trimming,
- Zgłaszanie błędów – zawsze informuj o poziomie optymalizacji i dostępnych ostrzeżeniach trimmingowych.
Studia przypadków oraz przykłady wdrożeń
Przykłady optymalizacji rozmiaru aplikacji .NET:
- Aplikacja webowa Enterprise – assembly-level trimming: pakiet z 150 MB na 85 MB, member-level: do 45 MB (wymagało poprawek kodu refleksyjnego);
- Mikrousługi – 20 usług, single-file plus assembly-level trimming; średni rozmiar usługi: 35 MB, obraz kontenera z 200 MB do 60 MB;
- Aplikacja desktopowa – Native AOT: plik 15 MB, wymagana wymiana komponentów na zgodne z trimmingiem;
- Backend mobilny – ReadyToRun i trimming: cold start z 2–3 s do 400 ms, Native AOT do 100 ms.
Aplikacje webowe i mikrousługi – praktyka:
- Stopniowe wdrożenie assembly-level trimming w ASP.NET Core self-contained: redukcja z 80 MB do 25 MB, optymalizacja dependency injection i konfiguracji: do 12 MB, po AOT – 9 MB,
- Kubernetes – 60% mniej RAM, szybszy start podów, mniejszy transfer aktualizacji,
- Integracja z gateway’ami API: wzrost wydajności przepływu,
- Health checki i circuit breakery – dostosowanie pod skrócony czas startu usług.
Aplikacje desktop i mobilne – efekty wdrożeń:
- WinForms: migracja do single-file z trimmingiem – z 45 MB do 18 MB, prosty deployment,
- WPF: wymiana kontrolek i trimming – z 95 MB do 40 MB,
- Backend mobilny na Azure: cold start ReadyToRun – 800 ms, po AOT – 150 ms,
- Avalonia cross-platform: build 25 MB na system zamiast 120 MB oryginału.
Przyszłe kierunki rozwoju
Przyszłość optymalizacji w .NET koncentruje się wokół:
- rozwoju Native AOT i kompatybilności z frameworkami,
- wspierania refleksji i skracania buildów,
- ulepszania pod cloud-native, edge computing, WebAssembly,
- integracji AI/ML do analizy i profilowania trymowania.
Zapowiedzi nowych funkcjonalności .NET
Dalszy rozwój optymalizacji to: większa kontrola trymowania, automatyzacja profilowania (profile-guided optimization), szybsza kompilacja WASM oraz adaptacja pod edge computing.