9 Tips to Reduce WPF App Startup Time

WPF Team Blog
26 January 2023

This post will document 9 strategies to improve cold start performance for your WPF app. The 3 techniques listed below apply to all WPF apps, regardless of components used:

  1. Ngen.exe
  2. MultiCore JIT
  3. ReadyToRun

The following techniques are specific to DevExpress WPF UI Controls:

  1. RibbonPage Deferred Loading
  2. Use LoadingDecorator for “Heavy” UI Elements
  3. Cache Tabs on Selection
  4. Load Data on Demand
  5. Preload Theme Resources

A final technique involves the use of Visual Studio's Performance Profiler (helps determine the underlying cause of performance-related issues).

1. Ngen.exe

Native Image Generator (Ngen.exe) is the first tool you should consider when optimizing startup for a .NET Framework app (.NET/.NET Core projects don’t support Ngen). As you may know, .NET Framework projects produce assemblies with Microsoft Intermediate Language (MSIL) code. This code needs to be translated to machine code before executing the application. Translation from MSIL to machine code begins at startup - a process that may require significant time.

You can use Ngen.exe to generate native image libraries that already contain native code. It’s important to note that Ngen.exe should be used on the machine where your application will be used. You can run it on your machine to test performance, but to optimize cold start for an end user, you need to use Ngen.exe on the user machine.

The best way to run Ngen on a user’s machine is to incorporate Ngen.exe within your application installer. You’ll need to execute the following line during the installation process and Ngen.exe will automatically process all assemblies associated with your project:

C:\Windows\Microsoft.NET\Framework\v4.0.30319\ngen.exe install C:\MyApp.exe

If you distribute your application as Click Once or an archive (and don’t have an installer), you can call Ngen.exe from code during application startup. To only run Ngen.exe for the first startup, calculate a hash for your executable file and check this hash during subsequent startups:

var savedHash = string.Empty;
var assemblyLocation = Assembly.GetEntryAssembly().Location;

// Specify a path to the file that stores your executable’s hash.
// Create this file or load the saved hash if the file already exists:
var hashPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "hash.txt");
if (!File.Exists(hashPath)) {
} else {
    savedHash = File.ReadAllText(hashPath);

// Obtain the hash for your executable.
// Cancel the operation if the application does not have changes:
var hash = string.Concat(SHA1.Create().ComputeHash(File.ReadAllBytes(assemblyLocation))
    .Select(x => x.ToString("x2")));
if (hash.Equals(savedHash))

// Obtain the path to ngen.exe:
var dotNetRuntimePath = RuntimeEnvironment.GetRuntimeDirectory();
var ngenPath = Path.Combine(dotNetRuntimePath, "ngen.exe");

// Create a process that runs ngen.exe:
var process = new Process {
    StartInfo = new ProcessStartInfo {
        FileName = ngenPath,
        // Pass the path to your executable:
        Arguments = $"install \"{assemblyLocation}\" /nologo",
        CreateNoWindow = true,
        WindowStyle = ProcessWindowStyle.Hidden,
        // Run the process as administrator:
        UseShellExecute = true,
        Verb = "runas"

// Run the process and save the executable’s hash:
try {
    File.WriteAllText(hashPath, hash);
catch {
    // Failed to start.
    // For example, a user cancelled the UAC prompt.

Windows 8 (and newer versions of the Windows OS) include a Native Image Task that automatically generates native images for frequently used .NET Framework 4.5+ applications when a computer is idle. Nonethless, you still need to run Ngen.exe manually, because Native Image Task only works with assemblies located in GAC or Windows Store app package.

The image below illustrates startup benefits you can expect from Ngen.exe:

2. MultiCore JIT

Alternatively, you can optimize the translation of Microsoft Intermediate Language (MSIL) to machine code by translating most frequently used methods asynchronously at application startup. You can use MultiCore JIT for this purpose (available for .NET and .NET Framework 4.5+ apps). MultiCore JIT logs methods used by your application and saves them to disk. When the application is executed for the second time, saved methods are compiled to native code in a separate process.

You simply need to use the following two lines of code in your app constructor to enable MultiCore JIT:

public App() {
    // Defines where to store JIT profiles
    // Enables Multicore JIT with the specified profile

MultiCore JIT is not as effective as Ngen.exe in terms of startup optimization. The good news is that it supports both .NET Framework and .NET/.NET Core projects. You can combine MultiCore JIT with the ReadyToRun option (described later) and benefit from both optimization strategies.

3. ReadyToRun

ReadyToRun (R2R) is a form of ahead-of-time (AOT) compilation. Applications published with R2R contain both MSIL and native code sections, which eliminates the need of just-in-time compilation (to a certain extent).

The R2R option is available only in .NET/.NET Core projects. To enable R2R, modify your “.csproj” / “.vbproj” file and add the PublishReadyToRun tag:


To proceed with this strategy, publish your project:

  1. Right-click the project and select Publish:

  2. In the subsequent dialog, select the target folder where the published application will reside.
  3. Open publish settings, select the target runtime and click the Publish button:

  4. Open the target folder and run your “.exe” file.

As mentioned earlier, R2R can be used together with MultiCore JIT in .NET/.NET Core applications. The image below illustrates startup benefits you can expect from R2R and MultiCore JIT.

4. RibbonPage Deferred Loading

If your application contains a DevExpress Ribbon Control with multiple items, on-demand page loading may help you reduce startup time.

To enable deferred RibbonPage item loading, move a RibbonPage’s RibbonPageGroups to the GroupCollectionTemplate.

<dxr:RibbonControl RibbonStyle="Office2019">
        <dxr:RibbonPage Caption="File">
            <!-- ... -->
        <dxr:RibbonPage Caption="Home">
                        <dxr:RibbonPageGroup Caption="File">
                            <dxb:BarButtonItem ... />
                            <dxb:BarButtonItem ... />

Please refer to the following help topic for additional information: Deferred RibbonPage Content Loading.

5. Use LoadingDecorator for “Heavy” UI Elements

Since WPF works with the UI in the main thread, the entire window remains frozen until all elements are loaded. The DevExpress LoadingDecorator allows you to load separate UI sections asynchronously:

By implementing this strategy, your app can display “lighter” UI elements before loading “heavier” elements on screen. In the animation above, I wrapped the right section of the view within LoadingDecorator to display our lightweight navigation menu first. The right section is displayed only when it’s fully loaded:

        <ColumnDefinition Width="Auto"/>
        <ColumnDefinition Width="*"/>
    <dx:LoadingDecorator Grid.Column="1">

6. Cache Tabs on Selection

If your application uses a tabbed interface, you can fine-tune performance using different tab cache modes. This feature is available within our WPF Tab Control (DXTabControl) and our WPF DockLayoutManager (when tabbed DocumentGroups or LayoutGroups are used).

Tabs are not cached by default, which is fine for initial startup. However, if you decide to open the same tab a second time, its content will be loaded once again. To use previously loaded elements, cache tabs after selection. This behavior is controlled by the TabContentCacheMode property (for DXTabControl, LayoutGroup and DocumentGroup). Set TabContentCacheMode to CacheTabsOnSelecting to enable this strategy.

If tab switching is more important than startup performance, you can set TabContentCacheMode to CacheAllTabs. In this instance, all tabs will be loaded and cached together with the first tab. This may significantly increase view loading time, however other tabs will be switched faster, even if you open them for the first time.

7. Load Data on Demand

While your application may work with large amounts of data, your users may not need to view all data in one shot. Many of our WPF controls allow you to load data on demand. The following WPF DevExpress WPF components support virtualization/on-demand data loading:

8. Preload Theme Resources

DevExpress components contain many XAML resources used by themes. WPF parses all these resources when a component is displayed for the first time. You may have noticed that displaying the same control for the second time takes much less time even if this control was displayed previously in a different view.

DevExpress themes allow you to partially load theme resources before a component is displayed on-screen. Although this won’t speed up the main view, all child views will open faster. You can use the following code to implement this strategy:

using DevExpress.Xpf.Core;

public partial class App : Application {
    protected async override void OnStartup(StartupEventArgs e) {
        await ThemeManager.PreloadThemeResourceAsync("Office2019Colorful");

If your application uses a Splash Screen, you can preload theme resources within it. Please refer to the following example for more information on this startup strategy: Synchronous Theme Preload With Splashscreen.

9. Use a Performance Profiler to Detect Issues

Certain operations in View/ViewModel initialization may generate unwanted startup delays. These delays may occur if you synchronously communicate with a remote service, process large files, or execute other resource-intensive operations in a View/ViewModel constructor.

Visual Studio includes a built-in Performance Profiler to help determine the exact cause of performance-related issues. To discover time-consuming operations, follow the steps below:

  1. Go to Debug -> Performance Profiler in Visual Studio.
  2. Select the CPU Usage checkbox and click Start.
  3. When the application loads, click Stop Collection to view CPU usage data in the profiling tab. The profiler will automatically highlight problematic methods.

You can click individual methods to navigate to a detailed call tree.

As you can see from the image above (taken from a test application), significant resources were consumed by the ProcessData method (called by the ViewModel constructor). One way to address this issue is to process the operation asynchronously.

Refer to the following help topic for more information on the Visual Studio Profiler: First look at profiling tools (C#, Visual Basic, C++, F#).

Free DevExpress Products - Get Your Copy Today

The following free DevExpress product offers remain available. Should you have any questions about the free offers below, please submit a ticket via the DevExpress Support Center at your convenience. We'll be happy to follow-up.