Skip to content

Commit 943527d

Browse files
authored
[Pre4] Blazor WASM performance profiling (#35243)
1 parent b9dffd6 commit 943527d

24 files changed

+545
-257
lines changed

.openpublishing.redirection.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1397,6 +1397,11 @@
13971397
"source_path": "aspnetcore/blazor/host-and-deploy/webassembly/integrity-check-failures.md",
13981398
"redirect_url": "/aspnet/core/blazor/host-and-deploy/webassembly/bundle-caching-and-integrity-check-failures",
13991399
"redirect_document_id": false
1400+
},
1401+
{
1402+
"source_path": "aspnetcore/blazor/performance.md",
1403+
"redirect_url": "/aspnet/core/blazor/performance/",
1404+
"redirect_document_id": false
14001405
}
14011406
]
14021407
}

aspnetcore/blazor/components/event-handling.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -580,7 +580,7 @@ It's often convenient to close over additional values using C# method parameters
580580

581581
:::moniker-end
582582

583-
Creating a large number of event delegates in a loop may cause poor rendering performance. For more information, see <xref:blazor/performance#avoid-recreating-delegates-for-many-repeated-elements-or-components>.
583+
Creating a large number of event delegates in a loop may cause poor rendering performance. For more information, see <xref:blazor/performance/rendering#avoid-recreating-delegates-for-many-repeated-elements-or-components>.
584584

585585
Avoid using a loop variable directly in a lambda expression, such as `i` in the preceding `for` loop example. Otherwise, the same variable is used by all lambda expressions, which results in use of the same value in all lambdas. Capture the variable's value in a local variable. In the preceding example:
586586

aspnetcore/blazor/components/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1392,7 +1392,7 @@ You can factor out child components purely as a way of reusing rendering logic.
13921392
}
13931393
```
13941394

1395-
For more information, see [Reuse rendering logic](xref:blazor/performance#define-reusable-renderfragments-in-code).
1395+
For more information, see [Reuse rendering logic](xref:blazor/performance/rendering#define-reusable-renderfragments-in-code).
13961396

13971397
## Loop variables with component parameters and child content
13981398

aspnetcore/blazor/components/lifecycle.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,7 @@ If a disposable component doesn't use a <xref:System.Threading.CancellationToken
326326

327327
For more information on route parameters and constraints, see <xref:blazor/fundamentals/routing>.
328328

329-
For an example of implementing `SetParametersAsync` manually to improve performance in some scenarios, see <xref:blazor/performance#implement-setparametersasync-manually>.
329+
For an example of implementing `SetParametersAsync` manually to improve performance in some scenarios, see <xref:blazor/performance/rendering#implement-setparametersasync-manually>.
330330

331331
## After component render (`OnAfterRender{Async}`)
332332

aspnetcore/blazor/components/rendering.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ Components inherited from <xref:Microsoft.AspNetCore.Components.ComponentBase> s
3939

4040
In most cases, <xref:Microsoft.AspNetCore.Components.ComponentBase> conventions result in the correct subset of component rerenders after an event occurs. Developers aren't usually required to provide manual logic to tell the framework which components to rerender and when to rerender them. The overall effect of the framework's conventions is that the component receiving an event rerenders itself, which recursively triggers rerendering of descendant components whose parameter values may have changed.
4141

42-
For more information on the performance implications of the framework's conventions and how to optimize an app's component hierarchy for rendering, see <xref:blazor/performance#optimize-rendering-speed>.
42+
For more information on the performance implications of the framework's conventions and how to optimize an app's component hierarchy for rendering, see <xref:blazor/performance/rendering>.
4343

4444
::: moniker range=">= aspnetcore-8.0"
4545

@@ -147,7 +147,7 @@ Even if <xref:Microsoft.AspNetCore.Components.ComponentBase.ShouldRender%2A> is
147147

148148
::: moniker-end
149149

150-
For more information on performance best practices pertaining to <xref:Microsoft.AspNetCore.Components.ComponentBase.ShouldRender%2A>, see <xref:blazor/performance#avoid-unnecessary-rendering-of-component-subtrees>.
150+
For more information on performance best practices pertaining to <xref:Microsoft.AspNetCore.Components.ComponentBase.ShouldRender%2A>, see <xref:blazor/performance/rendering#avoid-unnecessary-rendering-of-component-subtrees>.
151151

152152
## `StateHasChanged`
153153

aspnetcore/blazor/components/templated-components.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,6 @@ Without using the `@key` directive attribute in the `TableTemplate` component, t
329329

330330
## Additional resources
331331

332-
* <xref:blazor/performance#define-reusable-renderfragments-in-code>
332+
* <xref:blazor/performance/rendering#define-reusable-renderfragments-in-code>
333333
* <xref:blazor/components/key>
334334
* [Blazor samples GitHub repository (`dotnet/blazor-samples`)](https://github.com/dotnet/blazor-samples) ([how to download](xref:blazor/fundamentals/index#sample-apps))

aspnetcore/blazor/globalization-localization.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ Adopting [invariant globalization](#invariant-globalization) only results in usi
143143
```
144144

145145
> [!NOTE]
146-
> [`<BlazorEnableTimeZoneSupport>`](xref:blazor/performance#disable-unused-features) overrides an earlier `<InvariantTimezone>` setting. We recommend removing the `<BlazorEnableTimeZoneSupport>` setting.
146+
> [`<BlazorEnableTimeZoneSupport>`](xref:blazor/performance/app-download-size#disable-unused-features) overrides an earlier `<InvariantTimezone>` setting. We recommend removing the `<BlazorEnableTimeZoneSupport>` setting.
147147
148148
:::moniker-end
149149

aspnetcore/blazor/host-and-deploy/configure-linker.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,4 +119,4 @@ For more information, see [I18N: Pnetlib Internationalization Framework Library
119119

120120
## Additional resources
121121

122-
* <xref:blazor/performance#intermediate-language-il-linking>
122+
<xref:blazor/performance/app-download-size#intermediate-language-il-linking>

aspnetcore/blazor/host-and-deploy/configure-trimmer.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,4 +178,4 @@ Because custom types are never trimmed by Blazor when an app is published, the c
178178
## Additional resources
179179

180180
* [Trim self-contained deployments and executables](/dotnet/core/deploying/trimming/trim-self-contained)
181-
* <xref:blazor/performance#intermediate-language-il-trimming>
181+
* <xref:blazor/performance/app-download-size#intermediate-language-il-trimming>

aspnetcore/blazor/javascript-interoperability/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Further JS interop guidance is provided in the following articles:
1818

1919
* <xref:blazor/js-interop/call-javascript-from-dotnet>
2020
* <xref:blazor/js-interop/call-dotnet-from-javascript>
21+
* <xref:blazor/performance/js-interop>
2122

2223
:::moniker range=">= aspnetcore-7.0"
2324

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
---
2+
title: ASP.NET Core Blazor app download size performance best practices
3+
author: guardrex
4+
description: Tips for reducing app download size in ASP.NET Core Blazor apps and avoiding common performance problems.
5+
monikerRange: '>= aspnetcore-3.1'
6+
ms.author: riande
7+
ms.custom: mvc
8+
ms.date: 05/02/2025
9+
uid: blazor/performance/app-download-size
10+
---
11+
# ASP.NET Core Blazor app download size performance best practices
12+
13+
[!INCLUDE[](~/includes/not-latest-version.md)]
14+
15+
:::moniker range=">= aspnetcore-6.0"
16+
17+
## Runtime relinking
18+
19+
For information on how runtime relinking minimizes an app's download size, see <xref:blazor/tooling/webassembly#runtime-relinking>.
20+
21+
:::moniker-end
22+
23+
## Use `System.Text.Json`
24+
25+
Blazor's JS interop implementation relies on <xref:System.Text.Json>, which is a high-performance JSON serialization library with low memory allocation. Using <xref:System.Text.Json> shouldn't result in additional app payload size over adding one or more alternate JSON libraries.
26+
27+
For migration guidance, see [How to migrate from `Newtonsoft.Json` to `System.Text.Json`](/dotnet/standard/serialization/system-text-json-migrate-from-newtonsoft-how-to).
28+
29+
## Intermediate Language (IL) trimming
30+
31+
*This section only applies to client-side Blazor scenarios.*
32+
33+
:::moniker range=">= aspnetcore-5.0"
34+
35+
Trimming unused assemblies from a Blazor WebAssembly app reduces the app's size by removing unused code in the app's binaries. For more information, see <xref:blazor/host-and-deploy/configure-trimmer>.
36+
37+
:::moniker-end
38+
39+
:::moniker range="< aspnetcore-5.0"
40+
41+
[Linking a Blazor WebAssembly app](xref:blazor/host-and-deploy/configure-linker) reduces the app's size by trimming unused code in the app's binaries. The Intermediate Language (IL) Linker is only enabled when building in `Release` configuration. To benefit from this, publish the app for deployment using the [`dotnet publish`](/dotnet/core/tools/dotnet-publish) command with the [-c|--configuration](/dotnet/core/tools/dotnet-publish#options) option set to `Release`:
42+
43+
```dotnetcli
44+
dotnet publish -c Release
45+
```
46+
47+
:::moniker-end
48+
49+
## Lazy load assemblies
50+
51+
*This section only applies to client-side Blazor scenarios.*
52+
53+
Load assemblies at runtime when the assemblies are required by a route. For more information, see <xref:blazor/webassembly-lazy-load-assemblies>.
54+
55+
## Compression
56+
57+
*This section only applies to Blazor WebAssembly apps.*
58+
59+
When a Blazor WebAssembly app is published, the output is statically compressed during publish to reduce the app's size and remove the overhead for runtime compression. Blazor relies on the server to perform content negotiation and serve statically-compressed files.
60+
61+
After an app is deployed, verify that the app serves compressed files. Inspect the **Network** tab in a browser's [developer tools](https://developer.mozilla.org/docs/Glossary/Developer_Tools) and verify that the files are served with `Content-Encoding: br` (Brotli compression) or `Content-Encoding: gz` (Gzip compression). If the host isn't serving compressed files, follow the instructions in <xref:blazor/host-and-deploy/webassembly/index#compression>.
62+
63+
## Disable unused features
64+
65+
*This section only applies to client-side Blazor scenarios.*
66+
67+
Blazor WebAssembly's runtime includes the following .NET features that can be disabled for a smaller payload size.
68+
69+
Blazor WebAssembly carries globalization resources required to display values, such as dates and currency, in the user's culture. If the app doesn't require localization, you may configure the app to [support the invariant culture](xref:blazor/globalization-localization#invariant-globalization), which is based on the `en-US` culture. Apply the `<InvariantGlobalization>` MSBuild property with a value of `true` in the app's project file (`.csproj`):
70+
71+
```xml
72+
<PropertyGroup>
73+
<InvariantGlobalization>true</InvariantGlobalization>
74+
</PropertyGroup>
75+
```
76+
77+
:::moniker range=">= aspnetcore-8.0"
78+
79+
Adopting [invariant globalization](xref:blazor/globalization-localization#invariant-globalization) only results in using non-localized timezone names. To trim timezone code and data from the app, apply the `<InvariantTimezone>` MSBuild property with a value of `true` in the app's project file (`.csproj`):
80+
81+
```xml
82+
<PropertyGroup>
83+
<InvariantTimezone>true</InvariantTimezone>
84+
</PropertyGroup>
85+
```
86+
87+
> [!NOTE]
88+
> [`<BlazorEnableTimeZoneSupport>`](xref:blazor/performance/app-download-size#disable-unused-features) overrides an earlier `<InvariantTimezone>` setting. We recommend removing the `<BlazorEnableTimeZoneSupport>` setting.
89+
90+
:::moniker-end
91+
92+
:::moniker range="< aspnetcore-8.0"
93+
94+
A data file is included to make timezone information correct. If the app doesn't require this feature, consider disabling it by setting the `<BlazorEnableTimeZoneSupport>` MSBuild property to `false` in the app's project file:
95+
96+
```xml
97+
<PropertyGroup>
98+
<BlazorEnableTimeZoneSupport>false</BlazorEnableTimeZoneSupport>
99+
</PropertyGroup>
100+
```
101+
102+
:::moniker-end
103+
104+
:::moniker range="< aspnetcore-5.0"
105+
106+
Collation information is included to make APIs such as <xref:System.StringComparison.InvariantCultureIgnoreCase?displayProperty=nameWithType> work correctly. If you're certain that the app doesn't require the collation data, consider disabling it by setting the `BlazorWebAssemblyPreserveCollationData` MSBuild property in the app's project file to `false`:
107+
108+
```xml
109+
<PropertyGroup>
110+
<BlazorWebAssemblyPreserveCollationData>false</BlazorWebAssemblyPreserveCollationData>
111+
</PropertyGroup>
112+
```
113+
114+
:::moniker-end
115+
116+
## Additional resources
117+
118+
[Configuring and hosting .NET WebAssembly applications](https://github.com/dotnet/runtime/blob/main/src/mono/wasm/features.md)
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
title: ASP.NET Core Blazor performance best practices
3+
author: guardrex
4+
description: Tips for increasing the performance of ASP.NET Core Blazor apps and avoiding common performance problems.
5+
monikerRange: '>= aspnetcore-3.1'
6+
ms.author: riande
7+
ms.custom: mvc
8+
ms.date: 04/16/2025
9+
uid: blazor/performance/index
10+
---
11+
# ASP.NET Core Blazor performance best practices
12+
13+
[!INCLUDE[](~/includes/not-latest-version.md)]
14+
15+
Blazor is optimized for high performance in most realistic application UI scenarios. However, the best performance depends on developers adopting the correct patterns and features.
16+
17+
> [!NOTE]
18+
> The code examples in this node of articles adopt [nullable reference types (NRTs) and .NET compiler null-state static analysis](xref:migration/50-to-60#nullable-reference-types-nrts-and-net-compiler-null-state-static-analysis), which are supported in ASP.NET Core in .NET 6 or later.
19+
20+
:::moniker range=">= aspnetcore-6.0"
21+
22+
## Ahead-of-time (AOT) compilation
23+
24+
Ahead-of-time (AOT) compilation compiles a Blazor app's .NET code directly into native WebAssembly for direct execution by the browser. AOT-compiled apps result in larger apps that take longer to download, but AOT-compiled apps usually provide better runtime performance, especially for apps that execute CPU-intensive tasks. For more information, see <xref:blazor/tooling/webassembly#ahead-of-time-aot-compilation>.
25+
26+
:::moniker-end
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
---
2+
title: ASP.NET Core Blazor JavaScript interoperability (JS interop) performance best practices
3+
author: guardrex
4+
description: Tips for improving JS interop performance in ASP.NET Core Blazor apps and avoiding common performance problems.
5+
monikerRange: '>= aspnetcore-3.1'
6+
ms.author: riande
7+
ms.custom: mvc
8+
ms.date: 04/16/2025
9+
uid: blazor/performance/js-interop
10+
---
11+
# ASP.NET Core Blazor JavaScript interoperability (JS interop) performance best practices
12+
13+
[!INCLUDE[](~/includes/not-latest-version.md)]
14+
15+
Calls between .NET and JavaScript require additional overhead because:
16+
17+
* Calls are asynchronous.
18+
* Parameters and return values are JSON-serialized to provide an easy-to-understand conversion mechanism between .NET and JavaScript types.
19+
20+
Additionally for server-side Blazor apps, these calls are passed across the network.
21+
22+
## Avoid excessively fine-grained calls
23+
24+
Since each call involves some overhead, it can be valuable to reduce the number of calls. Consider the following code, which stores a collection of items in the browser's [`localStorage`](https://developer.mozilla.org/docs/Web/API/Window/localStorage):
25+
26+
```csharp
27+
private async Task StoreAllInLocalStorage(IEnumerable<TodoItem> items)
28+
{
29+
foreach (var item in items)
30+
{
31+
await JS.InvokeVoidAsync("localStorage.setItem", item.Id,
32+
JsonSerializer.Serialize(item));
33+
}
34+
}
35+
```
36+
37+
The preceding example makes a separate JS interop call for each item. Instead, the following approach reduces the JS interop to a single call:
38+
39+
```csharp
40+
private async Task StoreAllInLocalStorage(IEnumerable<TodoItem> items)
41+
{
42+
await JS.InvokeVoidAsync("storeAllInLocalStorage", items);
43+
}
44+
```
45+
46+
The corresponding JavaScript function stores the whole collection of items on the client:
47+
48+
```javascript
49+
function storeAllInLocalStorage(items) {
50+
items.forEach(item => {
51+
localStorage.setItem(item.id, JSON.stringify(item));
52+
});
53+
}
54+
```
55+
56+
For Blazor WebAssembly apps, rolling individual JS interop calls into a single call usually only improves performance significantly if the component makes a large number of JS interop calls.
57+
58+
## Consider the use of synchronous calls
59+
60+
:::moniker range=">= aspnetcore-5.0"
61+
62+
### Call JavaScript from .NET
63+
64+
[!INCLUDE[](~/blazor/includes/js-interop/synchronous-js-interop-call-js.md)]
65+
66+
### Call .NET from JavaScript
67+
68+
[!INCLUDE[](~/blazor/includes/js-interop/synchronous-js-interop-call-dotnet.md)]
69+
70+
:::moniker-end
71+
72+
:::moniker range="< aspnetcore-5.0"
73+
74+
[!INCLUDE[](~/blazor/includes/js-interop/synchronous-js-interop-call-js.md)]
75+
76+
:::moniker-end
77+
78+
:::moniker range=">= aspnetcore-5.0 < aspnetcore-7.0"
79+
80+
## Consider the use of unmarshalled calls
81+
82+
*This section only applies to Blazor WebAssembly apps.*
83+
84+
When running on Blazor WebAssembly, it's possible to make unmarshalled calls from .NET to JavaScript. These are synchronous calls that don't perform JSON serialization of arguments or return values. All aspects of memory management and translations between .NET and JavaScript representations are left up to the developer.
85+
86+
> [!WARNING]
87+
> While using <xref:Microsoft.JSInterop.IJSUnmarshalledRuntime> has the least overhead of the JS interop approaches, the JavaScript APIs required to interact with these APIs are currently undocumented and subject to breaking changes in future releases.
88+
89+
```javascript
90+
function jsInteropCall() {
91+
return BINDING.js_to_mono_obj("Hello world");
92+
}
93+
```
94+
95+
```razor
96+
@inject IJSRuntime JS
97+
98+
@code {
99+
protected override void OnInitialized()
100+
{
101+
var unmarshalledJs = (IJSUnmarshalledRuntime)JS;
102+
var value = unmarshalledJs.InvokeUnmarshalled<string>("jsInteropCall");
103+
}
104+
}
105+
```
106+
107+
:::moniker-end
108+
109+
:::moniker range=">= aspnetcore-7.0"
110+
111+
## Use JavaScript `[JSImport]`/`[JSExport]` interop
112+
113+
JavaScript `[JSImport]`/`[JSExport]` interop for Blazor WebAssembly apps offers improved performance and stability over the JS interop API in framework releases prior to ASP.NET Core in .NET 7.
114+
115+
For more information, see <xref:blazor/js-interop/import-export-interop>.
116+
117+
:::moniker-end

0 commit comments

Comments
 (0)