Skip to content

Commit 0b35f03

Browse files
[Blazor] Improve auto render mode selection strategy (#49858)
1 parent 32536e4 commit 0b35f03

23 files changed

+769
-405
lines changed

src/Components/Web.JS/dist/Release/blazor.server.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/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.Server.Common.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { WebRendererId } from './Rendering/WebRendererId';
2020
import { RootComponentManager } from './Services/RootComponentManager';
2121

2222
let renderingFailed = false;
23+
let hasStarted = false;
2324
let connection: HubConnection;
2425
let circuit: CircuitDescriptor;
2526
let dispatcher: DotNet.ICallDispatcher;
@@ -33,7 +34,13 @@ export function setCircuitOptions(circuitUserOptions?: Partial<CircuitStartOptio
3334
userOptions = circuitUserOptions;
3435
}
3536

36-
export async function startCircuit(components?: ServerComponentDescriptor[] | RootComponentManager): Promise<void> {
37+
export async function startCircuit(components: RootComponentManager<ServerComponentDescriptor>): Promise<void> {
38+
if (hasStarted) {
39+
throw new Error('Blazor Server has already started.');
40+
}
41+
42+
hasStarted = true;
43+
3744
// Establish options to be used
3845
const options = resolveOptions(userOptions);
3946
const jsInitializer = await fetchAndInvokeInitializers(options);
@@ -62,7 +69,7 @@ export async function startCircuit(components?: ServerComponentDescriptor[] | Ro
6269
logger.log(LogLevel.Information, 'Starting up Blazor server-side application.');
6370

6471
const appState = discoverPersistedState(document);
65-
circuit = new CircuitDescriptor(components || [], appState || '');
72+
circuit = new CircuitDescriptor(components, appState || '');
6673

6774
// Configure navigation via SignalR
6875
Blazor._internal.navigationManager.listenForNavigationEvents((uri: string, state: string | undefined, intercepted: boolean): Promise<void> => {

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { CircuitStartOptions } from './Platform/Circuits/CircuitStartOptions';
77
import { setCircuitOptions, startCircuit } from './Boot.Server.Common';
88
import { ServerComponentDescriptor, discoverComponents } from './Services/ComponentDescriptorDiscovery';
99
import { DotNet } from '@microsoft/dotnet-js-interop';
10+
import { InitialRootComponentsList } from './Services/InitialRootComponentsList';
1011

1112
let started = false;
1213

@@ -19,7 +20,8 @@ function boot(userOptions?: Partial<CircuitStartOptions>): Promise<void> {
1920
setCircuitOptions(userOptions);
2021

2122
const serverComponents = discoverComponents(document, 'server') as ServerComponentDescriptor[];
22-
return startCircuit(serverComponents);
23+
const components = new InitialRootComponentsList(serverComponents);
24+
return startCircuit(components);
2325
}
2426

2527
Blazor.start = boot;

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

Lines changed: 13 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -9,152 +9,44 @@
99
// of interactive components
1010

1111
import { DotNet } from '@microsoft/dotnet-js-interop';
12-
import { setCircuitOptions, startCircuit } from './Boot.Server.Common';
13-
import { loadWebAssemblyPlatform, setWebAssemblyOptions, startWebAssembly } from './Boot.WebAssembly.Common';
12+
import { setCircuitOptions } from './Boot.Server.Common';
13+
import { setWebAssemblyOptions } from './Boot.WebAssembly.Common';
1414
import { shouldAutoStart } from './BootCommon';
1515
import { Blazor } from './GlobalExports';
1616
import { WebStartOptions } from './Platform/WebStartOptions';
1717
import { attachStreamingRenderingListener } from './Rendering/StreamingRendering';
18-
import { NavigationEnhancementCallbacks, attachProgressivelyEnhancedNavigationListener, isPerformingEnhancedPageLoad } from './Services/NavigationEnhancement';
19-
import { ComponentDescriptor } from './Services/ComponentDescriptorDiscovery';
20-
import { RootComponentManager, attachAutoModeResolver } from './Services/RootComponentManager';
21-
import { DescriptorHandler, attachComponentDescriptorHandler, registerAllComponentDescriptors } from './Rendering/DomMerging/DomSync';
22-
import { waitForRendererAttached } from './Rendering/WebRendererInteropMethods';
23-
import { WebRendererId } from './Rendering/WebRendererId';
24-
25-
enum WebAssemblyLoadingState {
26-
None = 0,
27-
Loading = 1,
28-
Loaded = 2,
29-
Starting = 3,
30-
Started = 4,
31-
}
18+
import { attachProgressivelyEnhancedNavigationListener } from './Services/NavigationEnhancement';
19+
import { WebRootComponentManager } from './Services/WebRootComponentManager';
20+
import { attachComponentDescriptorHandler, registerAllComponentDescriptors } from './Rendering/DomMerging/DomSync';
3221

3322
let started = false;
34-
let hasCircuitStarted = false;
35-
let webAssemblyLoadingState = WebAssemblyLoadingState.None;
36-
let autoModeTimeoutState: undefined | 'waiting' | 'timed out';
37-
const autoModeWebAssemblyTimeoutMilliseconds = 100;
38-
39-
const rootComponentManager = new RootComponentManager();
23+
const rootComponentManager = new WebRootComponentManager();
4024

4125
function boot(options?: Partial<WebStartOptions>) : Promise<void> {
4226
if (started) {
4327
throw new Error('Blazor has already started.');
4428
}
29+
4530
started = true;
4631

32+
Blazor._internal.loadWebAssemblyQuicklyTimeout = 3000;
33+
4734
setCircuitOptions(options?.circuit);
4835
setWebAssemblyOptions(options?.webAssembly);
4936

50-
const navigationEnhancementCallbacks: NavigationEnhancementCallbacks = {
51-
documentUpdated: handleUpdatedComponentDescriptors,
52-
};
53-
54-
const descriptorHandler: DescriptorHandler = {
55-
registerComponentDescriptor,
56-
};
57-
58-
attachComponentDescriptorHandler(descriptorHandler);
59-
attachStreamingRenderingListener(options?.ssr, navigationEnhancementCallbacks);
60-
attachAutoModeResolver(resolveAutoMode);
37+
attachComponentDescriptorHandler(rootComponentManager);
38+
attachStreamingRenderingListener(options?.ssr, rootComponentManager);
6139

6240
if (!options?.ssr?.disableDomPreservation) {
63-
attachProgressivelyEnhancedNavigationListener(navigationEnhancementCallbacks);
41+
attachProgressivelyEnhancedNavigationListener(rootComponentManager);
6442
}
6543

6644
registerAllComponentDescriptors(document);
67-
handleUpdatedComponentDescriptors();
45+
rootComponentManager.documentUpdated();
6846

6947
return Promise.resolve();
7048
}
7149

72-
function resolveAutoMode(): 'server' | 'webassembly' | null {
73-
if (webAssemblyLoadingState >= WebAssemblyLoadingState.Loaded) {
74-
// The WebAssembly runtime has loaded or is actively starting, so we'll use
75-
// WebAssembly for the component in question. We'll also start
76-
// the WebAssembly runtime if it hasn't already.
77-
startWebAssemblyIfNotStarted();
78-
return 'webassembly';
79-
}
80-
81-
if (autoModeTimeoutState === 'timed out') {
82-
// We've waited too long for WebAssembly to initialize, so we'll use the Server
83-
// render mode for the component in question. At some point if the WebAssembly
84-
// runtime finishes loading, we'll start using it again due to the earlier
85-
// check in this function.
86-
startCircuitIfNotStarted();
87-
return 'server';
88-
}
89-
90-
if (autoModeTimeoutState === undefined) {
91-
// The WebAssembly runtime hasn't loaded yet, and this is the first
92-
// time auto mode is being requested.
93-
// We'll wait a bit for the WebAssembly runtime to load before making
94-
// a render mode decision.
95-
autoModeTimeoutState = 'waiting';
96-
setTimeout(() => {
97-
autoModeTimeoutState = 'timed out';
98-
99-
// We want to ensure that we activate any markers whose render mode didn't get resolved
100-
// earlier.
101-
handleUpdatedComponentDescriptors();
102-
}, autoModeWebAssemblyTimeoutMilliseconds);
103-
}
104-
105-
return null;
106-
}
107-
108-
function registerComponentDescriptor(descriptor: ComponentDescriptor) {
109-
rootComponentManager.registerComponentDescriptor(descriptor);
110-
111-
if (descriptor.type === 'auto') {
112-
startLoadingWebAssemblyIfNotStarted();
113-
} else if (descriptor.type === 'server') {
114-
startCircuitIfNotStarted();
115-
} else if (descriptor.type === 'webassembly') {
116-
startWebAssemblyIfNotStarted();
117-
}
118-
}
119-
120-
function handleUpdatedComponentDescriptors() {
121-
const shouldAddNewRootComponents = !isPerformingEnhancedPageLoad();
122-
rootComponentManager.handleUpdatedRootComponents(shouldAddNewRootComponents);
123-
}
124-
125-
async function startCircuitIfNotStarted() {
126-
if (hasCircuitStarted) {
127-
return;
128-
}
129-
130-
hasCircuitStarted = true;
131-
await startCircuit(rootComponentManager);
132-
await waitForRendererAttached(WebRendererId.Server);
133-
handleUpdatedComponentDescriptors();
134-
}
135-
136-
async function startLoadingWebAssemblyIfNotStarted() {
137-
if (webAssemblyLoadingState >= WebAssemblyLoadingState.Loading) {
138-
return;
139-
}
140-
141-
webAssemblyLoadingState = WebAssemblyLoadingState.Loading;
142-
await loadWebAssemblyPlatform();
143-
webAssemblyLoadingState = WebAssemblyLoadingState.Loaded;
144-
}
145-
146-
async function startWebAssemblyIfNotStarted() {
147-
if (webAssemblyLoadingState >= WebAssemblyLoadingState.Starting) {
148-
return;
149-
}
150-
151-
webAssemblyLoadingState = WebAssemblyLoadingState.Starting;
152-
await startWebAssembly(rootComponentManager);
153-
await waitForRendererAttached(WebRendererId.WebAssembly);
154-
webAssemblyLoadingState = WebAssemblyLoadingState.Started;
155-
handleUpdatedComponentDescriptors();
156-
}
157-
15850
Blazor.start = boot;
15951
window['DotNet'] = DotNet;
16052

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

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,17 @@ import { addDispatchEventMiddleware } from './Rendering/WebRendererInteropMethod
1414
import { WebAssemblyComponentDescriptor, discoverPersistedState } from './Services/ComponentDescriptorDiscovery';
1515
import { receiveDotNetDataStream } from './StreamingInterop';
1616
import { WebAssemblyComponentAttacher } from './Platform/WebAssemblyComponentAttacher';
17+
import { MonoConfig } from 'dotnet';
1718
import { RootComponentManager } from './Services/RootComponentManager';
1819

1920
let options: Partial<WebAssemblyStartOptions> | undefined;
2021
let platformLoadPromise: Promise<void> | undefined;
22+
let hasStarted = false;
23+
24+
let resolveBootConfigPromise: (value: MonoConfig) => void;
25+
const bootConfigPromise = new Promise<MonoConfig>(resolve => {
26+
resolveBootConfigPromise = resolve;
27+
});
2128

2229
export function setWebAssemblyOptions(webAssemblyOptions?: Partial<WebAssemblyStartOptions>) {
2330
if (options) {
@@ -27,13 +34,19 @@ export function setWebAssemblyOptions(webAssemblyOptions?: Partial<WebAssemblySt
2734
options = webAssemblyOptions;
2835
}
2936

30-
export async function startWebAssembly(components?: WebAssemblyComponentDescriptor[] | RootComponentManager): Promise<void> {
37+
export async function startWebAssembly(components: RootComponentManager<WebAssemblyComponentDescriptor>): Promise<void> {
38+
if (hasStarted) {
39+
throw new Error('Blazor WebAssembly has already started.');
40+
}
41+
42+
hasStarted = true;
43+
3144
if (inAuthRedirectIframe()) {
3245
// eslint-disable-next-line @typescript-eslint/no-empty-function
3346
await new Promise(() => { }); // See inAuthRedirectIframe for explanation
3447
}
3548

36-
const platformLoadPromise = loadWebAssemblyPlatform();
49+
const platformLoadPromise = loadWebAssemblyPlatformIfNotStarted();
3750

3851
addDispatchEventMiddleware((browserRendererId, eventHandlerId, continuation) => {
3952
// It's extremely unusual, but an event can be raised while we're in the middle of synchronously applying a
@@ -98,10 +111,9 @@ export async function startWebAssembly(components?: WebAssemblyComponentDescript
98111

99112
// Leverage the time while we are loading boot.config.json from the network to discover any potentially registered component on
100113
// the document.
101-
const componentAttacher = new WebAssemblyComponentAttacher(components || []);
114+
const componentAttacher = new WebAssemblyComponentAttacher(components);
102115
Blazor._internal.registeredComponents = {
103116
getRegisteredComponentsCount: () => componentAttacher.getCount(),
104-
getId: (index) => componentAttacher.getId(index),
105117
getAssembly: (id) => componentAttacher.getAssembly(id),
106118
getTypeName: (id) => componentAttacher.getTypeName(id),
107119
getParameterDefinitions: (id) => componentAttacher.getParameterDefinitions(id) || '',
@@ -134,8 +146,12 @@ export async function startWebAssembly(components?: WebAssemblyComponentDescript
134146
api.invokeLibraryInitializers('afterStarted', [Blazor]);
135147
}
136148

137-
export function loadWebAssemblyPlatform(): Promise<void> {
138-
platformLoadPromise ??= monoPlatform.load(options ?? {});
149+
export function waitForBootConfigLoaded(): Promise<MonoConfig> {
150+
return bootConfigPromise;
151+
}
152+
153+
export function loadWebAssemblyPlatformIfNotStarted(): Promise<void> {
154+
platformLoadPromise ??= monoPlatform.load(options ?? {}, resolveBootConfigPromise);
139155
return platformLoadPromise;
140156
}
141157

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { WebAssemblyStartOptions } from './Platform/WebAssemblyStartOptions';
99
import { setWebAssemblyOptions, startWebAssembly } from './Boot.WebAssembly.Common';
1010
import { WebAssemblyComponentDescriptor, discoverComponents } from './Services/ComponentDescriptorDiscovery';
1111
import { DotNet } from '@microsoft/dotnet-js-interop';
12+
import { InitialRootComponentsList } from './Services/InitialRootComponentsList';
1213

1314
let started = false;
1415

@@ -21,7 +22,8 @@ async function boot(options?: Partial<WebAssemblyStartOptions>): Promise<void> {
2122
setWebAssemblyOptions(options);
2223

2324
const webAssemblyComponents = discoverComponents(document, 'webassembly') as WebAssemblyComponentDescriptor[];
24-
await startWebAssembly(webAssemblyComponents);
25+
const components = new InitialRootComponentsList(webAssemblyComponents);
26+
await startWebAssembly(components);
2527
}
2628

2729
Blazor.start = boot;

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@ interface IBlazor {
5757
attachRootComponentToElement?: (arg0: any, arg1: any, arg2: any, arg3: any) => void;
5858
registeredComponents?: {
5959
getRegisteredComponentsCount: () => number;
60-
getId: (index) => number;
6160
getAssembly: (id) => string;
6261
getTypeName: (id) => string;
6362
getParameterDefinitions: (id) => string;
@@ -74,6 +73,7 @@ interface IBlazor {
7473
receiveWebAssemblyDotNetDataStream?: (streamId: number, data: any, bytesRead: number, errorMessage: string) => void;
7574
receiveWebViewDotNetDataStream?: (streamId: number, data: any, bytesRead: number, errorMessage: string) => void;
7675
attachWebRendererInterop?: typeof attachWebRendererInterop;
76+
loadWebAssemblyQuicklyTimeout?: number;
7777

7878
// JSExport APIs
7979
dotNetExports?: {

src/Components/Web.JS/src/Platform/Circuits/CircuitManager.ts

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,14 @@ import { RootComponentManager } from '../../Services/RootComponentManager';
1111
export class CircuitDescriptor {
1212
public circuitId?: string;
1313

14-
public components: ServerComponentDescriptor[] | RootComponentManager;
14+
public componentManager: RootComponentManager<ServerComponentDescriptor>;
1515

1616
public applicationState: string;
1717

18-
public constructor(components: ServerComponentDescriptor[] | RootComponentManager, appState: string) {
18+
public constructor(componentManager: RootComponentManager<ServerComponentDescriptor>, appState: string) {
1919
this.circuitId = undefined;
2020
this.applicationState = appState;
21-
this.components = components;
21+
this.componentManager = componentManager;
2222
}
2323

2424
public reconnect(reconnection: signalR.HubConnection): Promise<boolean> {
@@ -45,10 +45,7 @@ export class CircuitDescriptor {
4545
return false;
4646
}
4747

48-
const componentsJson = this.components instanceof RootComponentManager
49-
? '[]'
50-
: JSON.stringify(this.components.map(c => descriptorToMarker(c)));
51-
48+
const componentsJson = JSON.stringify(this.componentManager.initialComponents.map(c => descriptorToMarker(c)));
5249
const result = await connection.invoke<string>(
5350
'StartCircuit',
5451
navigationManagerFunctions.getBaseURI(),
@@ -75,9 +72,7 @@ export class CircuitDescriptor {
7572
// ... or it may be a root component added by .NET
7673
const parsedSequence = Number.parseInt(sequenceOrIdentifier);
7774
if (!Number.isNaN(parsedSequence)) {
78-
const descriptor = this.components instanceof RootComponentManager
79-
? this.components.resolveRootComponent(parsedSequence, componentId)
80-
: this.components[parsedSequence];
75+
const descriptor = this.componentManager.resolveRootComponent(parsedSequence, componentId);
8176
return toLogicalRootCommentElement(descriptor);
8277
}
8378

src/Components/Web.JS/src/Platform/Mono/MonoPlatform.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,8 @@ function getValueU64(ptr: number) {
5151
}
5252

5353
export const monoPlatform: Platform = {
54-
load: function load(options: Partial<WebAssemblyStartOptions>) {
55-
return createRuntimeInstance(options);
54+
load: function load(options: Partial<WebAssemblyStartOptions>, onConfigLoaded?: (loadedConfig: MonoConfig) => void) {
55+
return createRuntimeInstance(options, onConfigLoaded);
5656
},
5757

5858
start: function start() {
@@ -174,7 +174,7 @@ async function importDotnetJs(startOptions: Partial<WebAssemblyStartOptions>): P
174174
return await import(/* webpackIgnore: true */ absoluteSrc);
175175
}
176176

177-
function prepareRuntimeConfig(options: Partial<WebAssemblyStartOptions>): DotnetModuleConfig {
177+
function prepareRuntimeConfig(options: Partial<WebAssemblyStartOptions>, onConfigLoadedCallback?: (loadedConfig: MonoConfig) => void): DotnetModuleConfig {
178178
const config: MonoConfig = {
179179
maxParallelDownloads: 1000000, // disable throttling parallel downloads
180180
enableDownloadRetry: false, // disable retry downloads
@@ -192,6 +192,8 @@ function prepareRuntimeConfig(options: Partial<WebAssemblyStartOptions>): Dotnet
192192

193193
Blazor._internal.getApplicationEnvironment = () => loadedConfig.applicationEnvironment!;
194194

195+
onConfigLoadedCallback?.(loadedConfig);
196+
195197
const initializerArguments = [options, loadedConfig.resources?.extensions ?? {}];
196198
await invokeLibraryInitializers('beforeStart', initializerArguments);
197199
};
@@ -210,9 +212,9 @@ function prepareRuntimeConfig(options: Partial<WebAssemblyStartOptions>): Dotnet
210212
return dotnetModuleConfig;
211213
}
212214

213-
async function createRuntimeInstance(options: Partial<WebAssemblyStartOptions>): Promise<void> {
215+
async function createRuntimeInstance(options: Partial<WebAssemblyStartOptions>, onConfigLoaded?: (loadedConfig: MonoConfig) => void): Promise<void> {
214216
const { dotnet } = await importDotnetJs(options);
215-
const moduleConfig = prepareRuntimeConfig(options);
217+
const moduleConfig = prepareRuntimeConfig(options, onConfigLoaded);
216218

217219
if (options.applicationCulture) {
218220
dotnet.withApplicationCulture(options.applicationCulture);

0 commit comments

Comments
 (0)