[blazor][wasm] JSExport for events#65897
Merged
pavelsavara merged 11 commits intodotnet:mainfrom Mar 23, 2026
Merged
Conversation
…bAssemblyCallQueue
This was referenced Mar 22, 2026
Contributor
There was a problem hiding this comment.
Pull request overview
This PR introduces a Blazor WebAssembly “JSExport-first” interop path for DOM event dispatch and root component updates, aiming to bypass JSON serialization and the DotNetDispatcher reflection pipeline for improved performance on high-frequency events.
Changes:
- Added
[JSExport]-based typed event dispatch entrypoints (plus a JSON fallback for custom/unhandled events) that schedule dispatch viaWebAssemblyCallQueue. - Switched
updateRootComponentsto prefer a[JSExport]path when running on WASM, falling back toinvokeMethodAsyncfor non-WASM renderers. - Removed the public
JSInteropMethodstype and updated navigation/root-component boot logic and tests accordingly.
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| src/Components/WebAssembly/WebAssembly/test/EventDispatchTest.cs | Adds unit tests for field-info creation and touch-point unflattening helpers. |
| src/Components/WebAssembly/WebAssembly/src/Services/DefaultWebAssemblyJSRuntime.Interop.cs | Moves/organizes JSExport interop entrypoints (Invoke/BeginInvoke/EndInvoke/byte arrays) into a partial. |
| src/Components/WebAssembly/WebAssembly/src/Services/DefaultWebAssemblyJSRuntime.EventDispatch.cs | Adds JSExport event dispatch fast paths, JSON fallback parsing, and helper methods. |
| src/Components/WebAssembly/WebAssembly/src/Services/DefaultWebAssemblyJSRuntime.cs | Updates trimming annotations (DynamicDependency) for the new JSExport entrypoints. |
| src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs | Wires the renderer instance into DefaultWebAssemblyJSRuntime for direct dispatch; cleans up on dispose. |
| src/Components/WebAssembly/WebAssembly/src/PublicAPI.Unshipped.txt | Records removal of JSInteropMethods APIs. |
| src/Components/WebAssembly/WebAssembly/src/Infrastructure/JSInteropMethods.cs | Removes the JSInteropMethods public API surface (file left as header-only). |
| src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs | Removes trimming dependencies tied to JSInteropMethods; minor formatting change. |
| src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHost.cs | Removes GC.KeepAlive(typeof(JSInteropMethods)) used to preserve JS-invoked methods. |
| src/Components/Web.JS/src/Rendering/WebRendererInteropMethods.ts | Adds WASM direct event dispatch via dotNetExports with typed JSExport calls + JSON fallback; updates updateRootComponents. |
| src/Components/Web.JS/src/GlobalExports.ts | Expands dotNetExports typings with typed dispatch methods and updated root-components method. |
| src/Components/Web.JS/src/Boot.WebAssembly.Common.ts | Updates navigation/root-components paths to use the new JSExport entrypoints. |
| src/Components/test/testassets/BasicTestApp/Index.razor | Adds the new test component to the selector list. |
| src/Components/test/testassets/BasicTestApp/ClipboardProgressErrorEventComponent.razor | Adds a test component for clipboard/progress/error events. |
| src/Components/test/E2ETest/Tests/EventTest.cs | Adds E2E coverage for clipboard/progress/error event dispatch via the new path. |
…AssemblyJSRuntime.EventDispatch.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…lyRenderer.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
pavelsavara
commented
Mar 23, 2026
This was referenced Mar 23, 2026
pavelsavara
added a commit
to pavelsavara/aspnetcore
that referenced
this pull request
Mar 30, 2026
This reverts commit 41822ee. Keep new tests
pavelsavara
added a commit
that referenced
this pull request
Mar 30, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Blazor WASM: Direct JSExport event dispatch
bypass JSON serialization
Summary
Adds direct
[JSExport]fast paths for dispatching DOM events from JavaScript to .NET in Blazor WebAssembly, bypassing the existing JSON serialization +DotNetDispatcherreflection pipeline. Covers all built-in event types. Custom events use a JSON JSExport fallback that still avoidsDotNetDispatcheroverhead. Also applies the same JSExport-first pattern toupdateRootComponents.Performance impact
The existing event dispatch path (
invokeMethodAsync('DispatchEventAsync', eventDescriptor, eventArgs)) incurs:eventDescriptor,eventArgs), serialize both to JSON strings viaJSON.stringifyDotNetDispatcher.BeginInvokeDotNetorInvokeDotNetDotNetObjectReferenceby ID, resolve the methodDispatchEventAsyncby name via reflection/caching, deserialize the JSON parametersJsonElement eventDescriptorandJsonElement eventArgs, switch oneventNamestring, call typed*Reader.Read(JsonElement)methods (e.g.MouseEventArgsReader.Read) to reconstructEventArgsrenderer.DispatchEventAsync(eventHandlerId, fieldInfo, eventArgs)The new path:
[JSExport]method directly (e.g.DispatchMouseEvent) passing individual primitive values — no object construction, no JSON.stringifyEventArgsdirectly from parameters, callrenderer.DispatchEventAsync— noDotNetDispatcher, no reflection, noJsonElementparsingThis eliminates all JSON serialization/deserialization,
DotNetDispatchermethod resolution,DotNetObjectReferencelookup, andWebEventData.Parseoverhead per event. For high-frequency events likemousemove,pointermove,scroll,input, andkeydown, this is a significant reduction in per-event cost.Event ordering and
WebAssemblyCallQueueThe JSExport methods dispatch events through
WebAssemblyCallQueue.Schedule(), which is the same queue used by all other JS-to-.NET calls in Blazor WebAssembly (render batch acknowledgments,BeginInvokeDotNet, etc.).This is necessary because:
WebAssemblyCallQueuereplicates this ordering guarantee on the client.WebAssemblyCallQueue.IsInProgress), incoming events must be deferred until the batch completes. Without queuing, a JSExport event call duringUpdateDisplayAsynccould trigger re-entrant rendering, corrupting the render tree.If
WebAssemblyCallQueueis idle (no call in progress),Scheduleexecutes the callback synchronously — so for the common case of a user click with no pending work, there is zero additional overhead from queuing.Covered event types (15 JSExport methods)
DispatchMouseEventDispatchKeyboardEventDispatchChangeEventStringDispatchChangeEventBoolDispatchChangeEventStringArraystring[]via[JSMarshalAs]+ fieldInfoDispatchFocusEventDispatchClipboardEventDispatchPointerEventDispatchWheelEventDispatchTouchEventdouble[]with stride 7 per TouchPoint × 3 listsDispatchDragEventDispatchProgressEventDispatchErrorEventDispatchEmptyEventDispatchEventJsonRenderer.GetEventArgsType()updateRootComponentsJSExport pathWebRendererInteropMethods.updateRootComponentsnow checks fordotNetExportsfirst and callsUpdateRootComponentsCoredirectly when available, bypassinginvokeMethodAsync. Falls back to the existinginvokeMethodAsync('UpdateRootComponents', ...)path for non-WASM renderers. All callers use fire-and-forget (none await the result), so the synchronous return type is safe.Fallback path
DispatchEventJsonserves as the JSExport-based fallback for events that don't have a dedicated typed method (custom events, unrecognized event types). It receives the event data as a JSON string and deserializes it usingRenderer.GetEventArgsType()+JsonSerializer.Deserialize. This still avoids theDotNetDispatcheroverhead — only the JSON deserialization step remains.For non-WASM renderers (Server, WebView),
dotNetExportsis unavailable and the entiredispatchEventDirect()path is skipped — events fall back to the originalinvokeMethodAsync('DispatchEventAsync', ...)path.Test coverage
Unit tests (11 tests in
EventDispatchTest.cs)CreateFieldInfo_ReturnsNull_WhenComponentIdIsZeroEventFieldInfocreated for componentId=0CreateFieldInfo_ReturnsFieldInfo_WithStringValueCreateFieldInfo_ReturnsFieldInfo_WithBoolTruetruefield value, uses cachedBoxedTrueCreateFieldInfo_ReturnsFieldInfo_WithBoolFalsefalsefield value, uses cachedBoxedFalseCreateFieldInfo_BoolValues_AreCachedAssert.Sameon repeated bool boxing (perf optimization)UnflattenTouchPoints_ReturnsNull_ForNullArrayUnflattenTouchPoints_ReturnsNull_ForEmptyArrayUnflattenTouchPoints_ReturnsSinglePointUnflattenTouchPoints_ReturnsMultiplePointsUnflattenTouchPoints_Fields_AreCorrectlyMappedUnflattenTouchPoints_Stride_IgnoresExtraElementsE2E test coverage (
src/Components/test/E2ETest)The following E2E tests exercise the JSExport dispatch paths when running in WebAssembly mode:
EventTest.csEventBubblingTest.csEventCallbackTest.csEventCallback<T>invocation via event dispatchEventCustomArgsTest.csDispatchEventJsonfallbackEventFlagsTest.csFormsTest.csBindTest.csInputFileTest.csVirtualizationTest.csJSRootComponentsTest.csupdateRootComponents— add/dispose/update root componentsNew E2E tests added in this PR (in
EventTest.csusingClipboardProgressErrorEventComponent):ClipboardEvents_CanTriggerDispatchClipboardEvent— dispatches copy, cut, paste viaClipboardEventProgressEvents_CanTriggerDispatchProgressEvent— dispatches progress, loadstart, loadend viaProgressEventErrorEvent_CanTriggerDispatchErrorEvent— dispatches error viaErrorEventThese tests use
ToggleExecutionModeServerFixture(defaults to Client/WASM mode) orBlazorWasmTestAppFixture, so they exercise the JSExport code paths.