What is trimming?

Because .NET Core supports self-contained deployment - runtime and framework libraries are deployed alongside the app, so the end use doesn't have to install .NET runtime. Even though this is a great feature if you want to distribute your app to the end users, it has the downside - the size.

This is why .NET team developed mechanism called assembly trimming. During the publish process ILLink (the tool that does the trimming) "walk" though the code and identify the assemblies that are used by the code and remove the rest from the result bundle.

In .NET 5, this tool goes even deeper, it removes the types and members inside assemblies that are not used by the application, reducing the size even more.

Here is the comparison for the Hello World app (dotnet new console):

Assembly Trimming Result .NET 5Trimming is not safe

The trimming does a static analysis of the code and can only identify types and members when they are statically referenced from the code.  But what about reflection? This is a catch. ILink tool will simple remove the assembly, member or type if you do not specify otherwise manually. That is why you should be really careful with enabling trimming and using it in production.

You might think that if you do not use reflection in your code, you are safe, but wait for a moment, there might be some other third party nuget package that rely on reflection. So extensive testing is a must when using trimming.

Enable Assembly-level trimming

This is a default behaviour when PublishTrimmed is specified. But you can explicitly specify TrimMode=CopyUsed it in the project file:


<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net5.0</TargetFramework>
        <RuntimeIdentifier>win10-x64</RuntimeIdentifier>
        <PublishTrimmed>true</PublishTrimmed
        <TrimMode>CopyUsed</TrimMode>
    </PropertyGroup>
</Project>

or on the command line:

dotnet publish -r win10-x64 -p:PublishTrimmed=True [-p:TrimMode=CopyUsed]

Exclude assembly from trimming

If (or when) ILink is failing to detect references, you can add the following part to the project file to prevent assembly from being trimmed (e.g. System.Configuration will not be trimmed)

    
<ItemGroup>
    <TrimmerRootAssembly Include="System.Security" />
</ItemGroup>

Enable Member-level trimming

In .NET 5, ILink can go two steps deeper and remove types and members that are not used from the assemblies. This might have a huge effect if only a small part of an assembly is used.

Of course, this has more risk you have to extensively test your application if you decide to use this feature (it is still experimental in .NET 5)

To enable member-level trimming, set TrimMode=Link on the console:

dotnet publish -r win10-x64 -p:PublishTrimmed=True -p:TrimMode=Link

or in the project file:


<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net5.0</TargetFramework>
        <RuntimeIdentifier>linux-x64</RuntimeIdentifier>
        <PublishTrimmed>true</PublishTrimmed
        <TrimMode>Link</TrimMode>
    </PropertyGroup>
</Project>

Here is the size comparison:

Member-level trimming resultsEnable Trimming in Visual Studio

It's quite simple, edit your publishing profile and enable trim unused assemblies option:

Visual studio publish profile with edit button.

Profile settings dialog with deployment mode, target runtime, and trim unused assemblies options highlighted.

Fine-Tune Trimming

If the trimmer can’t be assured of the correctness of the trim, it will provide warnings based on the code used. The problem for dynamic code is not that it’s broken in a trimmed environment, but that the trimmer needs to know which assemblies/types/members will be used. A set of attributes have been added that enables code to be annotated to tell the trimmer what code should be included, or which API usage should prevent trimming.

[DynamicallyAccessedMembers] - Applied to instances of System.Type (or strings containing a type name) to tell the trimmer which members of the type will be accessed dynamically.

[UnconditionalSuppressMessage] - Used to suppress a warning message from the trimmer if the use case is known to be safe. For example, if an “Equals” method is written by retrieving all the fields on a type and looping over them comparing each field. If a field is unused, and therefore trimmed, there is no need to compare that field for equality.

[RequiresUnreferencedCode] - Tells the trimmer that the method is incompatible with trimming and so warnings should be presented where the method is called. This will suppress warnings for the code paths that are called by this method reducing the noise, and making it clearer to the developer which methods they call are problematic.

[DynamicDependency] - Specifies an explicit dependency from a method to other code, if the method is preserved the other code will also be preserved. Used in cases where the dependency is not expressed by the method, but rather by external factors, like native code.

More to come

This article gives you the overview of the trimming, but there is  move to come. .NET team is working hard on improving .NET in general and trimming in particular.

You can already combine Ready2Run and trimming to fine tune your app size and startup speed.

They are looking at using source generators to move functionality from runtime reflection to build-time code generation.

Another potential place to reduce the app size is switching off some of the framework features. In the future releases we will see more and move framework features that can be "switched off" during the compile time. This mechanism is called  feature switches.

And of course, Blazor will benefit a lot from the app size reduction, since it's crucial for the end user experience to reduce the bundle size as much as possible.