Skip to content

[Pre4] Blazor WASM performance profiling #35243

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 29 commits into from
May 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .openpublishing.redirection.json
Original file line number Diff line number Diff line change
Expand Up @@ -1397,6 +1397,11 @@
"source_path": "aspnetcore/blazor/host-and-deploy/webassembly/integrity-check-failures.md",
"redirect_url": "/aspnet/core/blazor/host-and-deploy/webassembly/bundle-caching-and-integrity-check-failures",
"redirect_document_id": false
},
{
"source_path": "aspnetcore/blazor/performance.md",
"redirect_url": "/aspnet/core/blazor/performance/",
"redirect_document_id": false
}
]
}
2 changes: 1 addition & 1 deletion aspnetcore/blazor/components/event-handling.md
Original file line number Diff line number Diff line change
Expand Up @@ -577,7 +577,7 @@ It's often convenient to close over additional values using C# method parameters

:::moniker-end

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>.
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>.

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:

Expand Down
2 changes: 1 addition & 1 deletion aspnetcore/blazor/components/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -1392,7 +1392,7 @@ You can factor out child components purely as a way of reusing rendering logic.
}
```

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

## Loop variables with component parameters and child content

Expand Down
2 changes: 1 addition & 1 deletion aspnetcore/blazor/components/lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@ If a disposable component doesn't use a <xref:System.Threading.CancellationToken

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

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

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

Expand Down
4 changes: 2 additions & 2 deletions aspnetcore/blazor/components/rendering.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ Components inherited from <xref:Microsoft.AspNetCore.Components.ComponentBase> s

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.

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>.
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>.

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

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

::: moniker-end

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>.
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>.

## `StateHasChanged`

Expand Down
2 changes: 1 addition & 1 deletion aspnetcore/blazor/components/templated-components.md
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,6 @@ Without using the `@key` directive attribute in the `TableTemplate` component, t

## Additional resources

* <xref:blazor/performance#define-reusable-renderfragments-in-code>
* <xref:blazor/performance/rendering#define-reusable-renderfragments-in-code>
* <xref:blazor/components/key>
* [Blazor samples GitHub repository (`dotnet/blazor-samples`)](https://github.com/dotnet/blazor-samples) ([how to download](xref:blazor/fundamentals/index#sample-apps))
2 changes: 1 addition & 1 deletion aspnetcore/blazor/globalization-localization.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ Adopting [invariant globalization](#invariant-globalization) only results in usi
```

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

:::moniker-end

Expand Down
2 changes: 1 addition & 1 deletion aspnetcore/blazor/host-and-deploy/configure-linker.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,4 +119,4 @@ For more information, see [I18N: Pnetlib Internationalization Framework Library

## Additional resources

* <xref:blazor/performance#intermediate-language-il-linking>
<xref:blazor/performance/app-download-size#intermediate-language-il-linking>
2 changes: 1 addition & 1 deletion aspnetcore/blazor/host-and-deploy/configure-trimmer.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,4 +178,4 @@ Because custom types are never trimmed by Blazor when an app is published, the c
## Additional resources

* [Trim self-contained deployments and executables](/dotnet/core/deploying/trimming/trim-self-contained)
* <xref:blazor/performance#intermediate-language-il-trimming>
* <xref:blazor/performance/app-download-size#intermediate-language-il-trimming>
1 change: 1 addition & 0 deletions aspnetcore/blazor/javascript-interoperability/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Further JS interop guidance is provided in the following articles:

* <xref:blazor/js-interop/call-javascript-from-dotnet>
* <xref:blazor/js-interop/call-dotnet-from-javascript>
* <xref:blazor/performance/js-interop>

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

Expand Down
118 changes: 118 additions & 0 deletions aspnetcore/blazor/performance/app-download-size.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
---
title: ASP.NET Core Blazor app download size performance best practices
author: guardrex
description: Tips for reducing app download size in ASP.NET Core Blazor apps and avoiding common performance problems.
monikerRange: '>= aspnetcore-3.1'
ms.author: riande
ms.custom: mvc
ms.date: 05/02/2025
uid: blazor/performance/app-download-size
---
# ASP.NET Core Blazor app download size performance best practices

[!INCLUDE[](~/includes/not-latest-version.md)]

:::moniker range=">= aspnetcore-6.0"

## Runtime relinking

For information on how runtime relinking minimizes an app's download size, see <xref:blazor/tooling/webassembly#runtime-relinking>.

:::moniker-end

## Use `System.Text.Json`

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.

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).

## Intermediate Language (IL) trimming

*This section only applies to client-side Blazor scenarios.*

:::moniker range=">= aspnetcore-5.0"

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>.

:::moniker-end

:::moniker range="< aspnetcore-5.0"

[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`:

```dotnetcli
dotnet publish -c Release
```

:::moniker-end

## Lazy load assemblies

*This section only applies to client-side Blazor scenarios.*

Load assemblies at runtime when the assemblies are required by a route. For more information, see <xref:blazor/webassembly-lazy-load-assemblies>.

## Compression

*This section only applies to Blazor WebAssembly apps.*

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.

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>.

## Disable unused features

*This section only applies to client-side Blazor scenarios.*

Blazor WebAssembly's runtime includes the following .NET features that can be disabled for a smaller payload size.

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`):

```xml
<PropertyGroup>
<InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup>
```

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

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`):

```xml
<PropertyGroup>
<InvariantTimezone>true</InvariantTimezone>
</PropertyGroup>
```

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

:::moniker-end

:::moniker range="< aspnetcore-8.0"

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:

```xml
<PropertyGroup>
<BlazorEnableTimeZoneSupport>false</BlazorEnableTimeZoneSupport>
</PropertyGroup>
```

:::moniker-end

:::moniker range="< aspnetcore-5.0"

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`:

```xml
<PropertyGroup>
<BlazorWebAssemblyPreserveCollationData>false</BlazorWebAssemblyPreserveCollationData>
</PropertyGroup>
```

:::moniker-end

## Additional resources

[Configuring and hosting .NET WebAssembly applications](https://github.com/dotnet/runtime/blob/main/src/mono/wasm/features.md)
26 changes: 26 additions & 0 deletions aspnetcore/blazor/performance/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
title: ASP.NET Core Blazor performance best practices
author: guardrex
description: Tips for increasing the performance of ASP.NET Core Blazor apps and avoiding common performance problems.
monikerRange: '>= aspnetcore-3.1'
ms.author: riande
ms.custom: mvc
ms.date: 04/16/2025
uid: blazor/performance/index
---
# ASP.NET Core Blazor performance best practices

[!INCLUDE[](~/includes/not-latest-version.md)]

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.

> [!NOTE]
> 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.

:::moniker range=">= aspnetcore-6.0"

## Ahead-of-time (AOT) compilation

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>.

:::moniker-end
117 changes: 117 additions & 0 deletions aspnetcore/blazor/performance/javascript-interoperability.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
---
title: ASP.NET Core Blazor JavaScript interoperability (JS interop) performance best practices
author: guardrex
description: Tips for improving JS interop performance in ASP.NET Core Blazor apps and avoiding common performance problems.
monikerRange: '>= aspnetcore-3.1'
ms.author: riande
ms.custom: mvc
ms.date: 04/16/2025
uid: blazor/performance/js-interop
---
# ASP.NET Core Blazor JavaScript interoperability (JS interop) performance best practices

[!INCLUDE[](~/includes/not-latest-version.md)]

Calls between .NET and JavaScript require additional overhead because:

* Calls are asynchronous.
* Parameters and return values are JSON-serialized to provide an easy-to-understand conversion mechanism between .NET and JavaScript types.

Additionally for server-side Blazor apps, these calls are passed across the network.

## Avoid excessively fine-grained calls

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):

```csharp
private async Task StoreAllInLocalStorage(IEnumerable<TodoItem> items)
{
foreach (var item in items)
{
await JS.InvokeVoidAsync("localStorage.setItem", item.Id,
JsonSerializer.Serialize(item));
}
}
```

The preceding example makes a separate JS interop call for each item. Instead, the following approach reduces the JS interop to a single call:

```csharp
private async Task StoreAllInLocalStorage(IEnumerable<TodoItem> items)
{
await JS.InvokeVoidAsync("storeAllInLocalStorage", items);
}
```

The corresponding JavaScript function stores the whole collection of items on the client:

```javascript
function storeAllInLocalStorage(items) {
items.forEach(item => {
localStorage.setItem(item.id, JSON.stringify(item));
});
}
```

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.

## Consider the use of synchronous calls

:::moniker range=">= aspnetcore-5.0"

### Call JavaScript from .NET

[!INCLUDE[](~/blazor/includes/js-interop/synchronous-js-interop-call-js.md)]

### Call .NET from JavaScript

[!INCLUDE[](~/blazor/includes/js-interop/synchronous-js-interop-call-dotnet.md)]

:::moniker-end

:::moniker range="< aspnetcore-5.0"

[!INCLUDE[](~/blazor/includes/js-interop/synchronous-js-interop-call-js.md)]

:::moniker-end

:::moniker range=">= aspnetcore-5.0 < aspnetcore-7.0"

## Consider the use of unmarshalled calls

*This section only applies to Blazor WebAssembly apps.*

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.

> [!WARNING]
> 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.

```javascript
function jsInteropCall() {
return BINDING.js_to_mono_obj("Hello world");
}
```

```razor
@inject IJSRuntime JS

@code {
protected override void OnInitialized()
{
var unmarshalledJs = (IJSUnmarshalledRuntime)JS;
var value = unmarshalledJs.InvokeUnmarshalled<string>("jsInteropCall");
}
}
```

:::moniker-end

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

## Use JavaScript `[JSImport]`/`[JSExport]` interop

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.

For more information, see <xref:blazor/js-interop/import-export-interop>.

:::moniker-end
Loading