<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Performance | The .NET Blog</title><link>https://thedotnetblog.com/tags/performance/</link><description>Articles, tutorials and insights from the .NET community.</description><generator>Hugo</generator><language>en</language><managingEditor>@thedotnetblog (The .NET Blog)</managingEditor><webMaster>@thedotnetblog</webMaster><lastBuildDate>Tue, 26 May 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://thedotnetblog.com/tags/performance/index.xml" rel="self" type="application/rss+xml"/><item><title>.NET 11 Finally Fixes the Process API</title><link>https://thedotnetblog.com/news/emiliano-montesdeoca/dotnet-11-process-api-improvements-runandcapturetext/</link><pubDate>Tue, 26 May 2026 00:00:00 +0000</pubDate><author>Emiliano Montesdeoca</author><guid>https://thedotnetblog.com/news/emiliano-montesdeoca/dotnet-11-process-api-improvements-runandcapturetext/</guid><description>System.Diagnostics.Process gets its biggest update in years. RunAndCaptureTextAsync, KillOnParentExit, SafeProcessHandle APIs, and full control over standard handle redirection — no more deadlock boilerplate.</description><content:encoded>&lt;p&gt;Every .NET developer who has ever needed to spawn a process and capture its output has written some variation of the same dangerous boilerplate: async read from stdout, async read from stderr, &lt;code&gt;WaitForExitAsync&lt;/code&gt;, don&amp;rsquo;t forget to drain both streams or you&amp;rsquo;ll deadlock. It&amp;rsquo;s a well-known trap that&amp;rsquo;s been there for years.&lt;/p&gt;
&lt;p&gt;.NET 11 finally fixes this properly.&lt;/p&gt;
&lt;h2 id="runandcapturetextasync"&gt;RunAndCaptureTextAsync&lt;/h2&gt;
&lt;p&gt;The headline addition: a single static method that starts a process, captures stdout and stderr, and waits for exit without deadlocking.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RunAndCaptureTextAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;dotnet&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;--version&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StandardOutput&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;One call. No manual stream draining. No carefully positioned &lt;code&gt;WaitForExit&lt;/code&gt;. If you just need to run something and get its output, this is the API you want.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s also &lt;code&gt;Process.RunAsync&lt;/code&gt; for the case where you want to wait for exit without capturing output.&lt;/p&gt;
&lt;h2 id="killonparentexit"&gt;KillOnParentExit&lt;/h2&gt;
&lt;p&gt;A common problem with spawned processes: if the parent crashes or is killed, child processes keep running as orphans. &lt;code&gt;KillOnParentExit&lt;/code&gt; lets you declare at process start time that the child process should be terminated when the parent process exits.&lt;/p&gt;
&lt;p&gt;This is a feature that&amp;rsquo;s existed in platform-specific ways (job objects on Windows, prctl on Linux) but required p/invoke or third-party libraries to use from .NET. Now it&amp;rsquo;s a first-class property on &lt;code&gt;ProcessStartInfo&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="safeprocesshandle-based-apis"&gt;SafeProcessHandle-Based APIs&lt;/h2&gt;
&lt;p&gt;The new lightweight API surface is built around &lt;code&gt;SafeProcessHandle&lt;/code&gt; rather than the full &lt;code&gt;Process&lt;/code&gt; class. The full &lt;code&gt;Process&lt;/code&gt; class carries a lot of state and is difficult to trim — the &lt;code&gt;SafeProcessHandle&lt;/code&gt; path is more trimmer-friendly for applications that need to minimize output size (WASM, native AOT).&lt;/p&gt;
&lt;h2 id="full-control-over-handle-inheritance"&gt;Full Control Over Handle Inheritance&lt;/h2&gt;
&lt;p&gt;The update also adds fine-grained control over which handles a child process inherits and how standard handles are redirected. Previously you could redirect stdin/stdout/stderr but couldn&amp;rsquo;t specify exactly which handles to inherit at the OS level. The new APIs expose that control.&lt;/p&gt;
&lt;h2 id="why-this-matters"&gt;Why This Matters&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;Process&lt;/code&gt; class is used in tooling, build systems, test runners, and any application that shells out to other executables. The old API surface dated back to .NET Framework and was showing its age. This isn&amp;rsquo;t a breaking change — the old APIs still work — but new code should prefer the new surface.&lt;/p&gt;
&lt;p&gt;For trimmed applications or AOT compilation scenarios, the &lt;code&gt;SafeProcessHandle&lt;/code&gt; path is particularly welcome. The old &lt;code&gt;Process&lt;/code&gt; class brought in a lot of reflection-heavy code that complicated trimming.&lt;/p&gt;
&lt;p&gt;Original post: &lt;a href="https://devblogs.microsoft.com/dotnet/process-api-improvements-in-dotnet-11/"&gt;Process API Improvements in .NET 11&lt;/a&gt;&lt;/p&gt;</content:encoded></item><item><title>How Copilot Studio Migrated to .NET 10 WebAssembly and Got 20% Faster</title><link>https://thedotnetblog.com/news/emiliano-montesdeoca/copilot-studio-net10-webassembly-migration-performance/</link><pubDate>Sat, 23 May 2026 00:00:00 +0000</pubDate><author>Emiliano Montesdeoca</author><guid>https://thedotnetblog.com/news/emiliano-montesdeoca/copilot-studio-net10-webassembly-migration-performance/</guid><description>.NET 10 WASM improvements aren't just for new projects. Here's what Copilot Studio measured after upgrading from .NET 8: automatic fingerprinting, WasmStripILAfterAOT by default, and real execution performance numbers.</description><content:encoded>&lt;p&gt;The Copilot Studio team did something every Blazor WASM developer has been curious about: they actually upgraded a production application from .NET 8 to .NET 10 and measured the results. The post shares specific numbers, which is rare and genuinely useful.&lt;/p&gt;
&lt;h2 id="the-upgrade-was-boring-thats-a-good-thing"&gt;The Upgrade Was Boring (That&amp;rsquo;s a Good Thing)&lt;/h2&gt;
&lt;p&gt;Updating target framework, refreshing package references, fixing any breaking changes. That&amp;rsquo;s it. The .NET 10 build is now running in production. The migration itself wasn&amp;rsquo;t the interesting part — the changes in .NET 10 are.&lt;/p&gt;
&lt;h2 id="automatic-asset-fingerprinting"&gt;Automatic Asset Fingerprinting&lt;/h2&gt;
&lt;p&gt;Previously, shipping a WASM app meant writing custom scripts to rename published assets with SHA256 hashes for cache-busting. Copilot Studio had a PowerShell script doing exactly this — rename files, inject &lt;code&gt;integrity&lt;/code&gt; attributes into the JavaScript loader, manage the whole thing manually.&lt;/p&gt;
&lt;p&gt;In .NET 10, all of that is built in. Published assets are automatically fingerprinted, imported directly from &lt;code&gt;dotnet.js&lt;/code&gt;, and integrity-validated without manual intervention. The team deleted the renaming script.&lt;/p&gt;
&lt;p&gt;Small change in scope, significant reduction in complexity.&lt;/p&gt;
&lt;h2 id="wasmstripilafteraot-is-now-on-by-default"&gt;WasmStripILAfterAOT Is Now On by Default&lt;/h2&gt;
&lt;p&gt;In .NET 8, stripping IL from AOT-compiled assemblies was opt-in. In .NET 10 it&amp;rsquo;s the default. After AOT compilation, the original IL bytecode is removed from the output — it isn&amp;rsquo;t needed at runtime, and keeping it was inflating package size for no reason.&lt;/p&gt;
&lt;p&gt;Copilot Studio uses a specific optimization: it ships both a JIT engine (fast startup) and an AOT engine (maximum steady-state performance), loading both in parallel and handing off from JIT to AOT once it&amp;rsquo;s ready. It also deduplicates files that are identical between the two engines.&lt;/p&gt;
&lt;p&gt;The new IL stripping behavior means AOT assemblies no longer match their JIT counterparts bit-for-bit, so fewer files are deduplicated:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;.NET 8: 59 shared files&lt;/li&gt;
&lt;li&gt;.NET 10: 22 shared files&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Net result: roughly 15% larger package size for the AOT engine. The AOT download is ~6% slower on fast LAN, ~17% slower on 4G. But it all happens in the background after the app is already interactive.&lt;/p&gt;
&lt;h2 id="the-performance-numbers"&gt;The Performance Numbers&lt;/h2&gt;
&lt;p&gt;This is the part that matters:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;~20% faster&lt;/strong&gt; on the first call (cold path)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;~5% faster&lt;/strong&gt; on subsequent calls (warm path)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The improvements are most visible in &amp;ldquo;big bots&amp;rdquo; — large, complex agents where AOT-compiled code dominates. For simpler workflows the gain is smaller.&lt;/p&gt;
&lt;h2 id="if-youre-still-on-net-8"&gt;If You&amp;rsquo;re Still on .NET 8&lt;/h2&gt;
&lt;p&gt;The migration story is genuinely simple: update &lt;code&gt;&amp;lt;TargetFramework&amp;gt;&lt;/code&gt;, refresh package references, remove any custom fingerprinting scripts, and you&amp;rsquo;ll automatically benefit from &lt;code&gt;WasmStripILAfterAOT&lt;/code&gt;. If you&amp;rsquo;re AOT-compiling, expect similar performance gains.&lt;/p&gt;
&lt;p&gt;One note from the post: if you load the .NET WASM runtime inside a &lt;code&gt;WebWorker&lt;/code&gt;, set &lt;code&gt;dotnetSidecar = true&lt;/code&gt; when initializing.&lt;/p&gt;
&lt;p&gt;Original post: &lt;a href="https://devblogs.microsoft.com/dotnet/copilot-studio-dotnet-10-migration/"&gt;Copilot Studio gets faster with .NET 10 on WebAssembly&lt;/a&gt;&lt;/p&gt;</content:encoded></item></channel></rss>