Skip to content

Commit d5a8d01

Browse files
[Blazor] Auto render mode improvements (#53159)
1 parent 71ecce4 commit d5a8d01

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

4241
// Defined here to avoid inadvertently imported enhanced navigation
4342
// related APIs in WebAssembly or Blazor Server contexts.

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

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

8382
// JSExport APIs
8483
dotNetExports?: {

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-runtime';
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
@@ -597,22 +597,6 @@ public void DynamicallyAddedSsrComponents_CanGetRemoved_BeforeStreamingRendering
597597
AssertBrowserLogDoesNotContainErrors();
598598
}
599599

600-
[Fact]
601-
public void AutoRenderMode_UsesBlazorServer_IfWebAssemblyResourcesTakeTooLongToLoad()
602-
{
603-
Navigate(ServerPathBase);
604-
Browser.Equal("Hello", () => Browser.Exists(By.TagName("h1")).Text);
605-
ForceWebAssemblyResourceCacheMiss();
606-
BlockWebAssemblyResourceLoad();
607-
608-
Navigate($"{ServerPathBase}/streaming-interactivity");
609-
Browser.Equal("Not streaming", () => Browser.FindElement(By.Id("status")).Text);
610-
611-
Browser.Click(By.Id(AddAutoPrerenderedId));
612-
Browser.Equal("True", () => Browser.FindElement(By.Id("is-interactive-0")).Text);
613-
Browser.Equal("Server", () => Browser.FindElement(By.Id("render-mode-0")).Text);
614-
}
615-
616600
[Fact]
617601
public void AutoRenderMode_UsesBlazorWebAssembly_AfterAddingWebAssemblyRootComponent()
618602
{
@@ -659,8 +643,6 @@ public void AutoRenderMode_UsesBlazorServerOnFirstLoad_ThenWebAssemblyOnSuccessi
659643
Navigate(ServerPathBase);
660644
Browser.Equal("Hello", () => Browser.Exists(By.TagName("h1")).Text);
661645
BlockWebAssemblyResourceLoad();
662-
UseLongWebAssemblyLoadTimeout();
663-
ForceWebAssemblyResourceCacheMiss();
664646

665647
Navigate($"{ServerPathBase}/streaming-interactivity");
666648
Browser.Equal("Not streaming", () => Browser.FindElement(By.Id("status")).Text);
@@ -697,8 +679,6 @@ public void AutoRenderMode_UsesBlazorServer_IfBootResourceHashChanges()
697679
Navigate(ServerPathBase);
698680
Browser.Equal("Hello", () => Browser.Exists(By.TagName("h1")).Text);
699681
BlockWebAssemblyResourceLoad();
700-
UseLongWebAssemblyLoadTimeout();
701-
ForceWebAssemblyResourceCacheMiss();
702682

703683
Navigate($"{ServerPathBase}/streaming-interactivity");
704684
Browser.Equal("Not streaming", () => Browser.FindElement(By.Id("status")).Text);
@@ -715,14 +695,11 @@ public void AutoRenderMode_UsesBlazorServer_IfBootResourceHashChanges()
715695
Browser.Click(By.Id($"remove-counter-link-1"));
716696
Browser.DoesNotExist(By.Id("is-interactive-1"));
717697

718-
UseLongWebAssemblyLoadTimeout();
719698
Browser.Navigate().Refresh();
720699

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

724-
BlockWebAssemblyResourceLoad();
725-
UseLongWebAssemblyLoadTimeout();
726703
ForceWebAssemblyResourceCacheMiss("dummy hash");
727704
Browser.Navigate().Refresh();
728705

@@ -766,8 +743,6 @@ public void AutoRenderMode_CanUseBlazorServer_WhenMultipleAutoComponentsAreAdded
766743
Navigate(ServerPathBase);
767744
Browser.Equal("Hello", () => Browser.Exists(By.TagName("h1")).Text);
768745
BlockWebAssemblyResourceLoad();
769-
UseLongWebAssemblyLoadTimeout();
770-
ForceWebAssemblyResourceCacheMiss();
771746

772747
Navigate($"{ServerPathBase}/streaming-interactivity");
773748
Browser.Equal("Not streaming", () => Browser.FindElement(By.Id("status")).Text);
@@ -911,6 +886,36 @@ public void AutoRenderMode_UsesBlazorServer_AfterWebAssemblyComponentsNoLongerEx
911886
Browser.Equal("Server", () => Browser.FindElement(By.Id("render-mode-3")).Text);
912887
}
913888

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

11361141
private void BlockWebAssemblyResourceLoad()
11371142
{
1143+
// Force a WebAssembly resource cache miss so that we can fall back to using server interactivity
1144+
ForceWebAssemblyResourceCacheMiss();
1145+
11381146
((IJavaScriptExecutor)Browser).ExecuteScript("sessionStorage.setItem('block-load-boot-resource', 'true')");
11391147

11401148
// Clear caches so that we can block the resource load
@@ -1146,11 +1154,6 @@ private void UnblockWebAssemblyResourceLoad()
11461154
((IJavaScriptExecutor)Browser).ExecuteScript("window.unblockLoadBootResource()");
11471155
}
11481156

1149-
private void UseLongWebAssemblyLoadTimeout()
1150-
{
1151-
((IJavaScriptExecutor)Browser).ExecuteScript("sessionStorage.setItem('use-long-auto-timeout', 'true')");
1152-
}
1153-
11541157
private void ForceWebAssemblyResourceCacheMiss(string resourceHash = null)
11551158
{
11561159
if (resourceHash is not null)
@@ -1164,6 +1167,11 @@ private void ForceWebAssemblyResourceCacheMiss(string resourceHash = null)
11641167
}
11651168
}
11661169

1170+
private long GetMaxParallelWebAssemblyResourceDownloadCount()
1171+
{
1172+
return (long)((IJavaScriptExecutor)Browser).ExecuteScript("return window['__aspnetcore__testing__max__parallel__resource__download__count'] || 0;");
1173+
}
1174+
11671175
private string InteractiveCallsiteUrl(bool prerender, int? serverIncrement = default, int? webAssemblyIncrement = default)
11681176
{
11691177
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)