Repository navigation
Description
Follow-up to #132966 (closed and locked) and its fix #134124 (merged to main, also locked), so we could not ask there. Avalonia side: AvaloniaUI/Avalonia#22663.
We hit this in production on .NET 10.0.13 (browser-wasm, single-threaded, interpreter). Avalonia.Browser 12.x exports its timer callbacks (TimerHelper.JsExportOnTimeout / JsExportOnInterval) and its pointer, wheel and key handlers as [JSExport] methods returning Task, and they nearly always complete synchronously. Avalonia's dispatcher calls the timer ones roughly 370 times a second on an idle page, so every Avalonia browser app leaks continuously:
- live JS heap after a forced full GC, idle page: 4.5 MB at load, 10.7 MB at 1 min, 37.5 MB at 5 min, 66.7 MB at 10 min (about 6 MB/min);
- the browser's major GCs then freeze the main thread for 2–6 s (Chrome and Safari/WebKit), getting worse the longer the tab is open;
- a heap snapshot after 60 s shows about 21,000 retained promise holders with their Promises and resolve/reject closures.
Minimal repro (no Avalonia):
// Microsoft.NET.Sdk.WebAssembly, net10.0, AllowUnsafeBlocks
public static partial class Exports
{
[JSExport]
public static Task Tick() => Task.CompletedTask;
}const runtime = await dotnet.create();
await runtime.runMain();
const exports = await runtime.getAssemblyExports('Repro.dll');
for (let i = 0; i < 100000; i++) exports.Exports.Tick();Live JS heap after a forced GC (Chrome, Release publish): 2.20 MB at start, 34.71 MB after 100,000 calls, 66.94 MB after 200,000.
Applying the two-statement change from #134124 to the shipped dotnet.runtime.js makes the same run flat: 2.20 → 2.57 → 2.58 MB. The change is that the eager Promise carries its holder's JSHandle, and end_marshal_task_to_js releases that handle instead of mono_wasm_get_js_handle(eagerPromise). With it, an idle Avalonia app stays at about 5.5 MB for 10 minutes. We currently patch the file at build time; a 10.0.x servicing fix would let every Blazor/Avalonia browser app drop such workarounds. Is a backport planned?
Activity
dotnet-policy-service commentedon Nov 2, 2026
Tagging subscribers to 'arch-wasm': @lewing, @pavelsavara
See info in area-owners.md if you want to be subscribed.
pavelsavara commentedon Nov 2, 2026
We are going to consider Net11 backport #136846
I think the memory leak doesn't meet the servicing bar for Net10, especially when it's probably there for a long time.
Also it looks like Avalonia bug to call it some many times. Are they missing a debounce ?
dotnet-policy-service commentedon Nov 4, 2026
This issue has been marked
needs-author-actionand may be missing some important information.robsonfelix commentedon Nov 4, 2026
Thanks @pavelsavara. Two points on the servicing bar, and an answer on the Avalonia side.
Why 10.0 servicing. .NET 10 is the LTS, so apps will stay on it until late 2028. A backport to 11.0 doesn't reach them. The cost is not just memory. The heap grows without bound, and the browser's major GCs then freeze the main thread for 2–6 s in Chrome and Safari, worse the longer the tab stays open. Any long-lived page hits this: dashboards, kiosks, back-office apps. The fix is the two-statement change from #134124, already on main and in the 11.0 backport, and we have run it patched into the shipped
dotnet.runtime.jswithout problems.On the call rate. Agreed that Avalonia shouldn't need ~370 calls a second on an idle page. That's AvaloniaUI/Avalonia#22663, and we've asked them to return
voidfrom those exports. But the leak is per call, not per rate. Any Task-returning[JSExport]that completes synchronously leaks one Promise, so a lower rate only slows it down. Pointer-move and wheel handlers legitimately fire at 60–120 Hz while the user is active.If 10.0 servicing is still a no, could you confirm that changing such exports to return
voidis the supported workaround on 10.0? Then libraries can switch to it without waiting for a runtime fix.