Skip to content

Commit 49439cf

Browse files
committed
[Blazor] Auto render mode improvements (#53159)
1 parent a07fe32 commit 49439cf

File tree

8 files changed

+110
-60
lines changed

8 files changed

+110
-60
lines changed

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
{
@@ -1161,6 +1166,9 @@ public void NavigationManagerCanRefreshSSRPageWhenServerInteractivityEnabled()
11611166

11621167
private void BlockWebAssemblyResourceLoad()
11631168
{
1169+
// Force a WebAssembly resource cache miss so that we can fall back to using server interactivity
1170+
ForceWebAssemblyResourceCacheMiss();
1171+
11641172
((IJavaScriptExecutor)Browser).ExecuteScript("sessionStorage.setItem('block-load-boot-resource', 'true')");
11651173

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

1175-
private void UseLongWebAssemblyLoadTimeout()
1176-
{
1177-
((IJavaScriptExecutor)Browser).ExecuteScript("sessionStorage.setItem('use-long-auto-timeout', 'true')");
1178-
}
1179-
11801183
private void ForceWebAssemblyResourceCacheMiss(string resourceHash = null)
11811184
{
11821185
if (resourceHash is not null)
@@ -1190,6 +1193,11 @@ private void ForceWebAssemblyResourceCacheMiss(string resourceHash = null)
11901193
}
11911194
}
11921195

1196+
private long GetMaxParallelWebAssemblyResourceDownloadCount()
1197+
{
1198+
return (long)((IJavaScriptExecutor)Browser).ExecuteScript("return window['__aspnetcore__testing__max__parallel__resource__download__count'] || 0;");
1199+
}
1200+
11931201
private string InteractiveCallsiteUrl(bool prerender, int? serverIncrement = default, int? webAssemblyIncrement = default)
11941202
{
11951203
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)