Skip to content

Commit c53fa3a

Browse files
author
Mackinnon Buck
committed
Merged PR 36408: [release/8.0] [Blazor] Auto render mode improvements
# Auto render mode improvements Backport of #53159 Improves the Auto render mode so that components are more responsive and have a decreased initial time to interactivity when WebAssembly resources are not already cached. ## Description One of the goals of the Auto render mode was to allow apps to become interactive as quickly as possible via Server interactivity, while WebAssembly bits were downloaded in the background for use on future visits to the site. However, since WebAssembly resources were being downloaded with maximal parallelism, the quality of the websocket connection required for Server interactivity was negatively impacted, often to the extent that the websocket wouldn't connect until WebAssembly resources had finished downloading completely, largely defeating the purpose of the Auto render mode. This PR makes the following improvements: * Removes a problematic timeout on loading the WebAssembly boot config. This fixes a problem where Server interactivity was always being used when the boot config took too long to load. * Introduces a limit to the maximum parallel WebAssembly resource downloads when an Auto component initiates the startup of the WebAssembly runtime. This limit is set to 1 and overrides any user-specified limit. * Fixes an issue where the circuit sometimes remains open even if WebAssembly gets selected for Auto interactivity. I provided a preview of these changes in #52154 (comment) so that customers could try them out, and the feedback so far has been very positive. Fixes #52154 ## Customer Impact A significant number of customers reported being affected by this problem in issues like #52154. I supplied customers with a preview of the fix that they could patch it into their app, and many indicated that their problems were resolved by the fix. The Auto render mode was one of the key features released in .NET 8, so it's important that it works in the way we've been advertising.   ## Regression? - [ ] Yes - [X] No ## Risk - [ ] High - [ ] Medium - [X] Low The core Auto render mode functionality is unaffected by this change - we added small tweaks to adjust the throttling amount and remove a problematic timeout. Additional tests were added to verify the changes in behavior, and we've been testing these changes manually to ensure they work well in real-world scenarios (various device types and connection qualities). ## Verification - [X] Manual (required) - [X] Automated ## Packaging changes reviewed? - [ ] Yes - [ ] No - [X] N/A
1 parent 436c63a commit c53fa3a

File tree

9 files changed

+111
-61
lines changed

9 files changed

+111
-61
lines changed

src/Components/Web.JS/dist/Release/blazor.web.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Components/Web.JS/src/Boot.Web.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ function boot(options?: Partial<WebStartOptions>) : Promise<void> {
3737
started = true;
3838
options = options || {};
3939
options.logLevel ??= LogLevel.Error;
40-
Blazor._internal.loadWebAssemblyQuicklyTimeout = 3000;
4140
Blazor._internal.isBlazorWeb = true;
4241

4342
// Defined here to avoid inadvertently imported enhanced navigation

src/Components/Web.JS/src/GlobalExports.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,6 @@ export interface IBlazor {
7979
receiveWebAssemblyDotNetDataStream?: (streamId: number, data: any, bytesRead: number, errorMessage: string) => void;
8080
receiveWebViewDotNetDataStream?: (streamId: number, data: any, bytesRead: number, errorMessage: string) => void;
8181
attachWebRendererInterop?: typeof attachWebRendererInterop;
82-
loadWebAssemblyQuicklyTimeout?: number;
8382
isBlazorWeb?: boolean;
8483

8584
// JSExport APIs

src/Components/Web.JS/src/Services/WebRootComponentManager.ts

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import { disposeCircuit, hasStartedServer, isCircuitAvailable, startCircuit, sta
99
import { hasLoadedWebAssemblyPlatform, hasStartedLoadingWebAssemblyPlatform, hasStartedWebAssembly, isFirstUpdate, loadWebAssemblyPlatformIfNotStarted, resolveInitialUpdate, setWaitForRootComponents, startWebAssembly, updateWebAssemblyRootComponents, waitForBootConfigLoaded } from '../Boot.WebAssembly.Common';
1010
import { MonoConfig } from 'dotnet';
1111
import { RootComponentManager } from './RootComponentManager';
12-
import { Blazor } from '../GlobalExports';
1312
import { getRendererer } from '../Rendering/Renderer';
1413
import { isPageLoading } from './NavigationEnhancement';
1514
import { setShouldPreserveContentOnInteractiveComponentDisposal } from '../Rendering/BrowserRenderer';
@@ -100,12 +99,18 @@ export class WebRootComponentManager implements DescriptorHandler, RootComponent
10099
return;
101100
}
102101

103-
if (descriptor.type === 'auto' || descriptor.type === 'webassembly') {
104-
// Eagerly start loading the WebAssembly runtime, even though we're not
105-
// activating the component yet. This is becuase WebAssembly resources
106-
// may take a long time to load, so starting to load them now potentially reduces
107-
// the time to interactvity.
102+
// When encountering a component with a WebAssembly or Auto render mode,
103+
// start loading the WebAssembly runtime, even though we're not
104+
// activating the component yet. This is becuase WebAssembly resources
105+
// may take a long time to load, so starting to load them now potentially reduces
106+
// the time to interactvity.
107+
if (descriptor.type === 'webassembly') {
108108
this.startLoadingWebAssemblyIfNotStarted();
109+
} else if (descriptor.type === 'auto') {
110+
// If the WebAssembly runtime starts downloading because an Auto component was added to
111+
// the page, we limit the maximum number of parallel WebAssembly resource downloads to 1
112+
// so that the performance of any Blazor Server circuit is minimally impacted.
113+
this.startLoadingWebAssemblyIfNotStarted(/* maxParallelDownloadsOverride */ 1);
109114
}
110115

111116
const ssrComponentId = this._nextSsrComponentId++;
@@ -120,26 +125,20 @@ export class WebRootComponentManager implements DescriptorHandler, RootComponent
120125
this.circuitMayHaveNoRootComponents();
121126
}
122127

123-
private async startLoadingWebAssemblyIfNotStarted() {
128+
private async startLoadingWebAssemblyIfNotStarted(maxParallelDownloadsOverride?: number) {
124129
if (hasStartedLoadingWebAssemblyPlatform()) {
125130
return;
126131
}
127132

128133
setWaitForRootComponents();
129134

130135
const loadWebAssemblyPromise = loadWebAssemblyPlatformIfNotStarted();
131-
132-
// If WebAssembly resources can't be loaded within some time limit,
133-
// we take note of this fact so that "auto" components fall back
134-
// to using Blazor Server.
135-
setTimeout(() => {
136-
if (!hasLoadedWebAssemblyPlatform()) {
137-
this.onWebAssemblyFailedToLoadQuickly();
138-
}
139-
}, Blazor._internal.loadWebAssemblyQuicklyTimeout);
140-
141136
const bootConfig = await waitForBootConfigLoaded();
142137

138+
if (maxParallelDownloadsOverride !== undefined) {
139+
bootConfig.maxParallelDownloads = maxParallelDownloadsOverride;
140+
}
141+
143142
if (!areWebAssemblyResourcesLikelyCached(bootConfig)) {
144143
// Since WebAssembly resources aren't likely cached,
145144
// they will probably need to be fetched over the network.
@@ -299,6 +298,8 @@ export class WebRootComponentManager implements DescriptorHandler, RootComponent
299298
this.updateWebAssemblyRootComponents(batchJson);
300299
}
301300
}
301+
302+
this.circuitMayHaveNoRootComponents();
302303
}
303304

304305
private updateWebAssemblyRootComponents(operationsJson: string) {

src/Components/test/E2ETest/ServerRenderingTests/InteractivityTest.cs

Lines changed: 38 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -598,22 +598,6 @@ public void DynamicallyAddedSsrComponents_CanGetRemoved_BeforeStreamingRendering
598598
AssertBrowserLogDoesNotContainErrors();
599599
}
600600

601-
[Fact]
602-
public void AutoRenderMode_UsesBlazorServer_IfWebAssemblyResourcesTakeTooLongToLoad()
603-
{
604-
Navigate(ServerPathBase);
605-
Browser.Equal("Hello", () => Browser.Exists(By.TagName("h1")).Text);
606-
ForceWebAssemblyResourceCacheMiss();
607-
BlockWebAssemblyResourceLoad();
608-
609-
Navigate($"{ServerPathBase}/streaming-interactivity");
610-
Browser.Equal("Not streaming", () => Browser.FindElement(By.Id("status")).Text);
611-
612-
Browser.Click(By.Id(AddAutoPrerenderedId));
613-
Browser.Equal("True", () => Browser.FindElement(By.Id("is-interactive-0")).Text);
614-
Browser.Equal("Server", () => Browser.FindElement(By.Id("render-mode-0")).Text);
615-
}
616-
617601
[Fact]
618602
public void AutoRenderMode_UsesBlazorWebAssembly_AfterAddingWebAssemblyRootComponent()
619603
{
@@ -661,8 +645,6 @@ public void AutoRenderMode_UsesBlazorServerOnFirstLoad_ThenWebAssemblyOnSuccessi
661645
Navigate(ServerPathBase);
662646
Browser.Equal("Hello", () => Browser.Exists(By.TagName("h1")).Text);
663647
BlockWebAssemblyResourceLoad();
664-
UseLongWebAssemblyLoadTimeout();
665-
ForceWebAssemblyResourceCacheMiss();
666648

667649
Navigate($"{ServerPathBase}/streaming-interactivity");
668650
Browser.Equal("Not streaming", () => Browser.FindElement(By.Id("status")).Text);
@@ -699,8 +681,6 @@ public void AutoRenderMode_UsesBlazorServer_IfBootResourceHashChanges()
699681
Navigate(ServerPathBase);
700682
Browser.Equal("Hello", () => Browser.Exists(By.TagName("h1")).Text);
701683
BlockWebAssemblyResourceLoad();
702-
UseLongWebAssemblyLoadTimeout();
703-
ForceWebAssemblyResourceCacheMiss();
704684

705685
Navigate($"{ServerPathBase}/streaming-interactivity");
706686
Browser.Equal("Not streaming", () => Browser.FindElement(By.Id("status")).Text);
@@ -717,14 +697,11 @@ public void AutoRenderMode_UsesBlazorServer_IfBootResourceHashChanges()
717697
Browser.Click(By.Id($"remove-counter-link-1"));
718698
Browser.DoesNotExist(By.Id("is-interactive-1"));
719699

720-
UseLongWebAssemblyLoadTimeout();
721700
Browser.Navigate().Refresh();
722701

723702
Browser.Equal("True", () => Browser.FindElement(By.Id("is-interactive-0")).Text);
724703
Browser.Equal("WebAssembly", () => Browser.FindElement(By.Id("render-mode-0")).Text);
725704

726-
BlockWebAssemblyResourceLoad();
727-
UseLongWebAssemblyLoadTimeout();
728705
ForceWebAssemblyResourceCacheMiss("dummy hash");
729706
Browser.Navigate().Refresh();
730707

@@ -768,8 +745,6 @@ public void AutoRenderMode_CanUseBlazorServer_WhenMultipleAutoComponentsAreAdded
768745
Navigate(ServerPathBase);
769746
Browser.Equal("Hello", () => Browser.Exists(By.TagName("h1")).Text);
770747
BlockWebAssemblyResourceLoad();
771-
UseLongWebAssemblyLoadTimeout();
772-
ForceWebAssemblyResourceCacheMiss();
773748

774749
Navigate($"{ServerPathBase}/streaming-interactivity");
775750
Browser.Equal("Not streaming", () => Browser.FindElement(By.Id("status")).Text);
@@ -913,6 +888,36 @@ public void AutoRenderMode_UsesBlazorServer_AfterWebAssemblyComponentsNoLongerEx
913888
Browser.Equal("Server", () => Browser.FindElement(By.Id("render-mode-3")).Text);
914889
}
915890

891+
[Fact]
892+
public void WebAssemblyRenderMode_DownloadsWebAssemblyResourcesInParallel()
893+
{
894+
Navigate($"{ServerPathBase}/streaming-interactivity?ClearSiteData=True");
895+
Browser.Equal("Not streaming", () => Browser.FindElement(By.Id("status")).Text);
896+
897+
Browser.Click(By.Id(AddWebAssemblyPrerenderedId));
898+
Browser.Equal("True", () => Browser.FindElement(By.Id("is-interactive-0")).Text);
899+
Browser.Equal("WebAssembly", () => Browser.FindElement(By.Id("render-mode-0")).Text);
900+
901+
Browser.True(() => GetMaxParallelWebAssemblyResourceDownloadCount() > 1);
902+
}
903+
904+
[Fact]
905+
public void AutoRenderMode_DoesNotDownloadWebAssemblyResourcesInParallel()
906+
{
907+
Navigate($"{ServerPathBase}/streaming-interactivity?ClearSiteData=True");
908+
Browser.Equal("Not streaming", () => Browser.FindElement(By.Id("status")).Text);
909+
910+
Browser.Click(By.Id(AddAutoPrerenderedId));
911+
Browser.Equal("True", () => Browser.FindElement(By.Id("is-interactive-0")).Text);
912+
Browser.Equal("Server", () => Browser.FindElement(By.Id("render-mode-0")).Text);
913+
914+
Browser.Click(By.Id(AddWebAssemblyPrerenderedId));
915+
Browser.Equal("True", () => Browser.FindElement(By.Id("is-interactive-1")).Text);
916+
Browser.Equal("WebAssembly", () => Browser.FindElement(By.Id("render-mode-1")).Text);
917+
918+
Browser.Equal(1, GetMaxParallelWebAssemblyResourceDownloadCount);
919+
}
920+
916921
[Fact]
917922
public void Circuit_ShutsDown_WhenAllBlazorServerComponentsGetRemoved()
918923
{
@@ -1169,6 +1174,9 @@ public void InteractiveServerRootComponent_CanAccessCircuitContext()
11691174

11701175
private void BlockWebAssemblyResourceLoad()
11711176
{
1177+
// Force a WebAssembly resource cache miss so that we can fall back to using server interactivity
1178+
ForceWebAssemblyResourceCacheMiss();
1179+
11721180
((IJavaScriptExecutor)Browser).ExecuteScript("sessionStorage.setItem('block-load-boot-resource', 'true')");
11731181

11741182
// Clear caches so that we can block the resource load
@@ -1180,11 +1188,6 @@ private void UnblockWebAssemblyResourceLoad()
11801188
((IJavaScriptExecutor)Browser).ExecuteScript("window.unblockLoadBootResource()");
11811189
}
11821190

1183-
private void UseLongWebAssemblyLoadTimeout()
1184-
{
1185-
((IJavaScriptExecutor)Browser).ExecuteScript("sessionStorage.setItem('use-long-auto-timeout', 'true')");
1186-
}
1187-
11881191
private void ForceWebAssemblyResourceCacheMiss(string resourceHash = null)
11891192
{
11901193
if (resourceHash is not null)
@@ -1198,6 +1201,11 @@ private void ForceWebAssemblyResourceCacheMiss(string resourceHash = null)
11981201
}
11991202
}
12001203

1204+
private long GetMaxParallelWebAssemblyResourceDownloadCount()
1205+
{
1206+
return (long)((IJavaScriptExecutor)Browser).ExecuteScript("return window['__aspnetcore__testing__max__parallel__resource__download__count'] || 0;");
1207+
}
1208+
12011209
private string InteractiveCallsiteUrl(bool prerender, int? serverIncrement = default, int? webAssemblyIncrement = default)
12021210
{
12031211
var result = $"{ServerPathBase}/interactive-callsite?suppress-autostart&prerender={prerender}";

src/Components/test/E2ETest/Tests/StatePersistenceTest.cs

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public StatePersistenceTest(
3131
public override Task InitializeAsync()
3232
=> InitializeAsync(BrowserFixture.StreamingContext + _nextStreamingIdContext++);
3333

34-
// Validates that we can use persisted state across server, webasembly, and auto modes, with and without
34+
// Validates that we can use persisted state across server, webassembly, and auto modes, with and without
3535
// streaming rendering.
3636
// For streaming rendering, we validate that the state is captured and restored after streaming completes.
3737
// For enhanced navigation we validate that the state is captured at the time components are rendered for
@@ -101,6 +101,12 @@ public void CanRenderComponentWithPersistedState(bool suppressEnhancedNavigation
101101
RenderComponentsWithPersistentStateAndValidate(suppressEnhancedNavigation, mode, renderMode, streaming, interactiveRuntime: "server");
102102

103103
UnblockWebAssemblyResourceLoad();
104+
105+
if (suppressEnhancedNavigation)
106+
{
107+
RenderWebAssemblyComponentAndWaitForWebAssemblyRuntime(returnUrl: Browser.Url);
108+
}
109+
104110
Browser.Navigate().Refresh();
105111

106112
RenderComponentsWithPersistentStateAndValidate(suppressEnhancedNavigation, mode, renderMode, streaming, interactiveRuntime: "wasm");
@@ -123,16 +129,19 @@ public async Task StateIsProvidedEveryTimeACircuitGetsCreated(string streaming)
123129
}
124130
Browser.Click(By.Id("page-with-components-link"));
125131

126-
RenderComponentsWithPersistentStateAndValidate(suppresEnhancedNavigation: false, mode, typeof(InteractiveServerRenderMode), streaming);
132+
RenderComponentsWithPersistentStateAndValidate(suppressEnhancedNavigation: false, mode, typeof(InteractiveServerRenderMode), streaming);
127133
Browser.Click(By.Id("page-no-components-link"));
128134
// Ensure that the circuit is gone.
129135
await Task.Delay(1000);
130136
Browser.Click(By.Id("page-with-components-link-and-state"));
131-
RenderComponentsWithPersistentStateAndValidate(suppresEnhancedNavigation: false, mode, typeof(InteractiveServerRenderMode), streaming, stateValue: "other");
137+
RenderComponentsWithPersistentStateAndValidate(suppressEnhancedNavigation: false, mode, typeof(InteractiveServerRenderMode), streaming, stateValue: "other");
132138
}
133139

134140
private void BlockWebAssemblyResourceLoad()
135141
{
142+
// Clear local storage so that the resource hash is not found
143+
((IJavaScriptExecutor)Browser).ExecuteScript("localStorage.clear()");
144+
136145
((IJavaScriptExecutor)Browser).ExecuteScript("sessionStorage.setItem('block-load-boot-resource', 'true')");
137146

138147
// Clear caches so that we can block the resource load
@@ -145,7 +154,7 @@ private void UnblockWebAssemblyResourceLoad()
145154
}
146155

147156
private void RenderComponentsWithPersistentStateAndValidate(
148-
bool suppresEnhancedNavigation,
157+
bool suppressEnhancedNavigation,
149158
string mode,
150159
Type renderMode,
151160
string streaming,
@@ -154,7 +163,7 @@ private void RenderComponentsWithPersistentStateAndValidate(
154163
{
155164
stateValue ??= "restored";
156165
// No need to navigate if we are using enhanced navigation, the tests will have already navigated to the page via a link.
157-
if (suppresEnhancedNavigation)
166+
if (suppressEnhancedNavigation)
158167
{
159168
// In this case we suppress auto start to check some server side state before we boot Blazor.
160169
if (streaming == null)
@@ -232,6 +241,18 @@ private void AssertPageState(
232241
}
233242
}
234243

244+
private void RenderWebAssemblyComponentAndWaitForWebAssemblyRuntime(string returnUrl = null)
245+
{
246+
Navigate("subdir/persistent-state/page-with-webassembly-interactivity");
247+
248+
Browser.Equal("True", () => Browser.FindElement(By.Id("is-interactive-counter")).Text);
249+
250+
if (returnUrl is not null)
251+
{
252+
Navigate(returnUrl);
253+
}
254+
}
255+
235256
private void SuppressEnhancedNavigation(bool shouldSuppress)
236257
=> EnhancedNavigationTestUtil.SuppressEnhancedNavigation(this, shouldSuppress);
237258
}

src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,8 @@
2121
const enableClassicInitializers = sessionStorage.getItem('enable-classic-initializers') === 'true';
2222
const suppressEnhancedNavigation = sessionStorage.getItem('suppress-enhanced-navigation') === 'true';
2323
const blockLoadBootResource = sessionStorage.getItem('block-load-boot-resource') === 'true';
24-
const useLongWebAssemblyTimeout = sessionStorage.getItem('use-long-auto-timeout') === 'true';
2524
sessionStorage.removeItem('suppress-enhanced-navigation');
2625
sessionStorage.removeItem('block-load-boot-resource');
27-
sessionStorage.removeItem('use-long-auto-timeout');
2826
sessionStorage.removeItem('enable-classic-initializers');
2927
3028
let loadBootResourceUnblocked = null;
@@ -34,6 +32,9 @@
3432
});
3533
}
3634
35+
let maxParallelResourceDownloadCount = 0;
36+
let currentParallelResourceDownloadCount = 0;
37+
3738
function callBlazorStart() {
3839
Blazor.start({
3940
ssr: {
@@ -55,19 +56,21 @@
5556
// The following allows us to arbitrarily delay the loading of WebAssembly resources.
5657
// This is useful for guaranteeing that auto mode components will fall back on
5758
// using Blazor server.
59+
currentParallelResourceDownloadCount++;
5860
return fetch(`${document.baseURI}WasmMinimal/_framework/${name}?`, {
5961
method: "GET",
6062
}).then(async (response) => {
63+
if (currentParallelResourceDownloadCount > maxParallelResourceDownloadCount) {
64+
maxParallelResourceDownloadCount = currentParallelResourceDownloadCount;
65+
window['__aspnetcore__testing__max__parallel__resource__download__count'] = maxParallelResourceDownloadCount;
66+
}
67+
currentParallelResourceDownloadCount--;
6168
await loadBootResourceUnblocked;
6269
return response;
6370
});
6471
}
6572
},
6673
},
67-
}).then(() => {
68-
if (useLongWebAssemblyTimeout) {
69-
Blazor._internal.loadWebAssemblyQuicklyTimeout = 10000000;
70-
}
7174
}).then(() => {
7275
const startedParagraph = document.createElement('p');
7376
startedParagraph.id = 'blazor-started';

src/Components/test/testassets/Components.TestServer/RazorComponents/Components/InteractiveStreamingRenderingComponent.razor

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,9 @@ else
100100
ComponentState _state = new(ImmutableArray<CounterInfo>.Empty, NextCounterId: 0);
101101
bool _isStreaming = false;
102102

103+
[CascadingParameter]
104+
public HttpContext HttpContext { get; set; }
105+
103106
[SupplyParameterFromQuery]
104107
public string? InitialState { get; set; }
105108

@@ -109,13 +112,21 @@ else
109112
[SupplyParameterFromQuery]
110113
public bool DisableKeys { get; set; }
111114

115+
[SupplyParameterFromQuery]
116+
public bool ClearSiteData { get; set; }
117+
112118
protected override async Task OnInitializedAsync()
113119
{
114120
if (InitialState is not null)
115121
{
116122
_state = ReadStateFromJson(InitialState);
117123
}
118124

125+
if (ClearSiteData)
126+
{
127+
HttpContext.Response.Headers["Clear-Site-Data"] = "\"*\"";
128+
}
129+
119130
if (ShouldStream)
120131
{
121132
_isStreaming = true;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
@page "/persistent-state/page-with-webassembly-interactivity"
2+
3+
<p>
4+
This page is used to ensure that the WebAssembly runtime is downloaded and available
5+
so that WebAssembly interactivity will get used for components with the Auto render mode
6+
</p>
7+
8+
<TestContentPackage.Counter @rendermode="RenderMode.InteractiveWebAssembly" IncrementAmount="1" IdSuffix="counter" />

0 commit comments

Comments
 (0)