By making changes in the following areas, I believe we can cut somewhere between 6 - 8 MB
of size on disk of the 32 MB
Albums Linux NativeAOT app.
- Compression -
1.1 MB
- System.Text.Json -
~2 MB
- Regex -
1.2 MB
- Linq.Expresions -
1.1 MB
- Non-Required Dependencies -
~2 MB
Cataloging the size on disk of the Albums app, shows there are a few places where large dependencies are brought into the app. The following diagram lists the largest dependencies and their respective sizes.
Note: interior boxes are dependencies that are pulled in by the outer boxes.
A large portion of the app (~22%) is taken up by supporting HTTPS/SSL/TLS. This is caused by chained dependencies.
In order to support HTTPS, we need System.Security.Cryptography. Cryptography needs to validate certificates, thus it needs to make outgoing HTTP requests - pulling in HttpClient. HTTP requests can be compressed, so HttpClient pulls in System.IO.Compression - both Brotli and Deflate.
- Remove HTTPS support in the web server
- Using a feature switch set in the .csproj
- Create a barebones ASP.NET API that doesn't enable HTTPS support by default. Users would need to explicitly ".AddHttps()" to bring it in.
- Remove Compression dependency from HttpClient
- We could do some internal refactoring that allowed the DecompressionHandler to be trimmed if no one set the AutomaticDecompression property (which defaults to None)
- Another possibility is using a feature switch in the .csproj.
- Note that this size savings comes from C/C++ code, so removing this dependency only really helps app size of NativeAOT apps, and not "regular" trimmed apps. Regular trimming doesn't trim native code.
- This savings would also accrue to any NativeAOT app that uses HttpClient without enabling compression.
- Remove HttpClient dependency from Cryptography
- The HTTP needs of Crypto are small. It could be possible to write a minimal HTTP implementation on top of Sockets to meet the needs of Cryptography. This would allow the whole HttpClient implementation to be trimmed from the server - but only if the application doesn't make other outbound HTTP requests.
System.Text.Json's design pulls in all the features by default, whether the app uses them or not. One example of this is the "converter" design where all converters are registered up-front. The JsonSerializer then uses Reflection to inspect the Types that get passed to it.
The way around this design is to use the JSON Source Generator. However, the source generator needs to be used everywhere in the app in order to get unused JSON features to be trimmed. There can't be any calls to the JsonSerializer without passing the source generated information. Once there is, all the JSON features are brought back into the app, and the size reduction is eliminated.
One proposal here that may help is Expose instance methods in JsonSerializer. This would allow for ASP.NET's internal code to get an instance of JsonSerializer and call Serialize/Deserialize as instance methods. The implementation of how to serialize and deserialize would depend on how the JsonSerializer instance was created. If it was created to only use source generated information, the unused JSON features could be trimmed. This also has the side effect of reducing trimming warnings, because the instance methods to Serialize/Deserialize wouldn't be marked as unsafe. Instead, it depends on how the JsonSerializer instance was constructed.
Routing has a feature named "Route Constraints". One of the options to make a constraint is to add a regular expression to the route, for example: app.MapGet("/posts/{id:regex(^[a-z0-9]+$)}", …)
. Because these route constraints are inline in the route string, the Regex code needs to always be in the application, in case any of the routes happen to use a regex constraint.
In .NET 7, we added a new feature to Regex: NonBacktracking
. This added a considerable amount of code. Depending on the Regex constructor overload used (the one that takes RegexOptions
, which ASP.NET Routing uses), this new feature's code will be left in the app, even if the NonBacktracking
engine isn't being used.
ASP.NET Routing uses the CultureInvariant
and IgnoreCase
options when constructing Regex route constraints.
Testing locally, being able to remove the NonBacktracking
engine can cut about 1.2 MB
of the 1.6 MB
of Regex code out of the app size.
UPDATE 11/30/2022
With the latest NativeAOT compiler changes, here are updated numbers for linux-x64 NativeAOT:
Hello World | 3.22 MB (3,381,112 bytes) |
new Regex("").IsMatch | 3.50 MB (3,680,200 bytes) |
new Regex("", RegexOptions).IsMatch | 4.33 MB (4,545,960 bytes) |
- Add new Regex construction APIs that allow for some
RegexOptions
to be used, but also allows for theNonBacktracking
engine to be trimmed. For example:Regex.CreateCompiled(pattern, RegexOptions)
. This API would throw an exception ifRegexOptions.NonBacktracking
was passed. - Remove the use of
RegexOptions
. TheIgnoreCase
option can be specified as part of the pattern as a Pattern Modifier:(?i)
. However,CultureInvariant
cannot be specified this way.- One option is to drop support for
CultureInvariant
from regex route constraints. This affects the Turkish 'i' handling. - Another option could be to add a
CultureInvariant
Pattern Modifier to .NET Regex, so this could be specified without usingRegexOptions
.
- One option is to drop support for
- We could remove the Regex route constraints feature either with an API that adds it back, or a feature switch set in the .csproj
- Add a feature to the linker/NativeAOT compiler that can see which RegexOptions values are used in the app. And for the enum values that aren't used, it can trim the code branches behind those values (i.e. the NonBacktracking code). This would accrue more size savings elsewhere as well.
There are 2 places that use System.Linq.Expressions to create dynamic methods:
- Minimal APIs to wrap the user's provided Delegate into a RequestDelegate
- This usage should be eliminated by the proposed Minimal APIs source generator
UseMiddleware<TMiddleware>
- We could potentially use a source generator here as well, to avoid the SLE usages.
- We could manually write code in our internal usages of UseMIddleware to remove the SLE usages
UPDATE 11/30/2022
With the latest NativeAOT compiler changes, here are updated numbers for linux-x64 NativeAOT:
Hello World | 3.22 MB (3,381,112 bytes) |
System.Linq.Expressions.Expression<Func<int, bool>> expr = i => i < 5; Func<int, bool> deleg = expr.Compile(); |
4.40 MB (4,615,096 bytes) |
Using the new WebHostBuilder()
(v1.0) API instead WebApplication.CreateBuilder()
eliminates many dependencies that aren't strictly required. The size savings is about 4.2 MB
out of the 32.7 MB
above. The assemblies eliminated are:
- Microsoft.AspNetCore.Authentication.Abstractions.dll
- Microsoft.AspNetCore.Authentication.Core.dll
- Microsoft.AspNetCore.Authentication.dll
- Microsoft.AspNetCore.Authorization.dll
- Microsoft.AspNetCore.Authorization.Policy.dll
- Microsoft.AspNetCore.Diagnostics.Abstractions.dll
- Microsoft.AspNetCore.Diagnostics.dll
- Microsoft.AspNetCore.dll
- Microsoft.AspNetCore.HostFiltering.dll
- Microsoft.AspNetCore.HttpOverrides.dll
- Microsoft.AspNetCore.Server.IIS.dll
- Microsoft.AspNetCore.Server.IISIntegration.dll
- Microsoft.Extensions.Configuration.Binder.dll
- Microsoft.Extensions.Configuration.CommandLine.dll
- Microsoft.Extensions.Configuration.Json.dll
- Microsoft.Extensions.Configuration.UserSecrets.dll
- Microsoft.Extensions.FileProviders.Composite.dll
- Microsoft.Extensions.Hosting.dll
- Microsoft.Extensions.Logging.Configuration.dll
- Microsoft.Extensions.Logging.Console.dll
- Microsoft.Extensions.Logging.Debug.dll
- Microsoft.Extensions.Logging.EventLog.dll
- Microsoft.Extensions.Logging.EventSource.dll
- Microsoft.Extensions.Options.ConfigurationExtensions.dll
- System.Collections.Specialized.dll
- System.Diagnostics.EventLog.dll
- System.Security.Principal.Windows.dll
Out of this list, the more critical ones are Authentication/Authorization, Diagnostics, and Console Logging. The Json Configuration (i.e. appsettings.json file) is also very useful to many apps, but not always required.
The work here is to prototype creating a new "Application Builder" API that depends on only the most critical features. Cloud developers using this API would opt-in to the extra features on top of that in their Program.cs file. This allows microservice apps to start off as small as possible, and only when the app needs a feature does it get brought into the app.