次の方法で共有


WinUI アプリのスタートアップ パフォーマンスのベスト プラクティス

Windows App SDK を使用して WinUI アプリを作成します。このアプリは、起動作業を減らし、最初のフレームを簡略化し、ウィンドウが対話型の後に重要でない機能を読み込むことで、すぐに開始できます。

アプリ起動時のパフォーマンスのベスト プラクティス

一部では、アプリの起動にかかる時間に基づいて、アプリの速度が速いか遅いかをユーザーが認識します。 このトピックでは、ユーザーがアプリを起動し、ユーザーが意味のある方法でアプリを操作できるようになると、アプリの起動時間が開始されます。 この記事では、WinUI アプリから起動パフォーマンスを向上させる方法に関する提案を提供します。

アプリの起動時間の測定

実際に起動時間を測定する前に、アプリを数回起動してください。 これにより、測定のベースラインが得られ、できるだけ短い起動時間で測定できるようになります。

エンド ユーザーが経験する内容を代表する測定値を取得します。 メジャー リリースは、代表的なハードウェアに基づいて構築され、コールド スタートアップとウォーム スタートアップの両方を確認し、プロセスが存在するまでの時間だけでなく、最初の対話型フレームまでの時間に焦点を当てます。

可能な限り作業を延期する

アプリの起動時間を短縮するには、ユーザーがアプリの操作を開始するために絶対に必要な作業のみを行います。 これは、追加のアセンブリの読み込みを遅らせることができる場合に特に便利です。 共通言語ランタイムは、初めて使用されるときにアセンブリを読み込みます。 読み込まれるアセンブリの数を最小限に抑えることができる場合は、アプリの起動時間とそのメモリ消費量を向上させることができます。

長時間行われる処理を独立して行う

アプリの一部が完全に機能していない場合でも、アプリを対話型にすることができます。 たとえば、取得に時間がかかるデータがアプリに表示される場合は、非同期的にデータを取得することで、そのコードをアプリのスタートアップ コードとは別に実行できます。 データが使用可能になったら、アプリのユーザー インターフェイスにデータを設定します。

データを取得する API の多くは非同期であるため、おそらく非同期的にデータを取得することになります。 詳細については、「 async と await を使用した非同期プログラミング」を参照してください。 非同期 API を使用しない作業を行う場合は、 Task クラスを使用して実行時間の長い作業を実行し、ユーザーがアプリを操作できないようにすることができます。 これにより、データの読み込み中にアプリの応答性が維持されます。

アプリが UI の一部を読み込むのに特に時間がかかる場合は、その領域に "最新のデータを取得する" などのメッセージを表示して、アプリがまだ処理されていることをユーザーに知ることを検討してください。

起動時間を最小限に抑える

最も単純なアプリ以外はすべて、起動中にリソースの読み込み、XAML の解析、データ構造の設定、ロジックの実行を行うために、目に見える時間が必要です。 WinUI アプリの場合は、プロセスの起動、ウィンドウの作成、メイン ページの作成、最初のフレームのレイアウト/レンダリングの 4 つの段階でスタートアップについて考えるのに役立ちます。

スタートアップ期間は、ユーザーがアプリを起動してからアプリが機能するまでの時間です。 これは、アプリに対するユーザーの第一印象であるため、重要な時期です。 ユーザーは、システムやアプリからの即時かつ継続的なフィードバックを期待します。 アプリがすぐに起動しない場合、システムとアプリは壊れているか、設計が不十分であると認識されます。

スタートアップのステージの概要

スタートアップには多数の移動要素が含まれており、それらのすべてが最適なユーザー エクスペリエンスを実現するために調整される必要があります。 次の手順は、アプリを起動するユーザーと表示されるアプリケーション コンテンツの間で発生します。

  • プロセスが起動し、テンプレートによって生成されたスタートアップ コードが Main呼び出されます。
  • Application オブジェクトが作成されます。
    • アプリ コンストラクターは InitializeComponentを呼び出します。これにより、 App.xaml が解析され、オブジェクトが作成されます。
  • Application.OnLaunched が発生します。
    • アプリ コードによってメイン ウィンドウが作成され、初期コンテンツが割り当てられ、 Activateが呼び出されます。
    • メイン ページ コンストラクターは InitializeComponentを呼び出します。これにより、ページ XAML が解析され、オブジェクトが作成されます。
  • XAML フレームワークは、計測および配置を含むレイアウト処理を実行します。
    • ApplyTemplate では、各コントロールに対してコントロール テンプレートのコンテンツが作成されます。これは通常、起動時のレイアウト時間の大部分です。
  • Render では、ウィンドウの内容のビジュアルが作成されます。
  • 最初のフレームが表示され、起動後の作業は非同期的に続行されます。

スタートアップの道をシンプルにする

最初のフレームに不要なものからスタートアップ コード パスを解放します。

  • 最初のフレームで不要なコントロールを含むユーザー DLL がある場合は、遅延読み込みを検討してください。
  • クラウドからのデータに依存する UI の一部がある場合は、その UI を分割します。 まず、クラウド データに依存しない UI を表示してから、クラウドに依存する UI を非同期的に表示します。 また、アプリケーションがオフラインで動作したり、低速なネットワーク接続の影響を受けないように、データをローカルにキャッシュすることも検討する必要があります。
  • UI がデータを待機している場合は、進行状況 UI を表示します。
  • 構成ファイルの解析が多いアプリ設計や、コードによって動的に生成される UI には注意してください。

要素数を減らす

XAML アプリのスタートアップ パフォーマンスは、起動時に作成する要素の数と直接関連付けられます。 作成する要素が少ないほど、アプリの起動にかかる時間が短くなります。 大まかなベンチマークとして、各要素の作成に 1 ミリ秒かかるとします。

  • 項目コントロールで使用されるテンプレートは、複数回繰り返されるため、最も大きな影響を与える可能性があります。 ListView と GridView UI の最適化を参照してください。
  • UserControl とコントロール テンプレートが展開されるため、それらを考慮する必要もあります。
  • 画面に表示されない XAML を作成する場合は、起動時にそれらの XAML を作成するかどうかを正当化する必要があります。

Visual Studio Live Visual Tree ウィンドウには、ツリー内の各ノードの子要素数が表示されます。

可視化ツリー。

延期を使用します。 要素を折りたたんだり、不透明度を 0 に設定したりしても、要素が作成されるのを防ぐことはありません。 x:Loadまたはx:DeferLoadStrategyを使用すると、UI の読み込みを遅らせ、必要に応じて読み込むことができます。 これは、必要に応じて、または遅延ロジックのセットの一部として読み込むことができるように、起動時に表示されない UI の処理を遅延させる良い方法です。 読み込みをトリガーするには、要素の FindName のみを呼び出す必要があります。 例と詳細については、 x:Load 属性x:DeferLoadStrategy 属性を参照してください。

仮想化。 UI にリストまたはリピーター コンテンツがある場合は、UI 仮想化を使用することを強くお勧めします。 リスト UI が仮想化されていない場合は、すべての要素を前もって作成するコストが発生し、起動速度が低下する可能性があります。 ListView と GridView UI の最適化を参照してください。

アプリケーションのパフォーマンスは生のパフォーマンスだけではありません。それは知覚についてもです。 視覚的な側面が最初に発生するように操作の順序を変更すると、通常、ユーザーはアプリケーションの方が速いように感じます。 ユーザーは、コンテンツが画面上にあるときにアプリケーションが読み込まれたと見なします。 通常、アプリケーションは起動時に複数のことを行う必要があります。UI を起動するためにすべての作業が必要なわけではないため、それらの部分は UI よりも遅れたり優先順位を付けたりする必要があります。

この記事では、アニメーションとビデオの用語に由来する 最初のフレームについて説明します。これは、エンド ユーザーがコンテンツを表示するまでにかかる時間の尺度です。

スタートアップの認識を向上させる

単純なオンライン ゲームの例を使用して、スタートアップの各フェーズと、プロセス全体を通してユーザーフィードバックを提供するさまざまな手法を特定しましょう。

最初のフェーズでは、プロセスが起動し、アプリによってウィンドウが作成されます。 この間、ユーザーはまだアプリ独自のコンテンツを見ていません。 あなたの目標は、画面上の軽量ウィンドウをすばやく取得することです。

2 番目のフェーズでは、ゲームにとって重要な構造の作成と初期化を含みます。 アプリが起動時に使用可能なデータを使用して初期 UI をすばやく作成できる場合、このフェーズは簡単であり、UI をすぐに表示できます。 それ以外の場合は、アプリの初期化中に軽量の読み込みページを表示します。

読み込みページの外観はユーザー次第です。プログレス バーやプログレス リングを表示するのと同じくらい簡単です。 重要な点は、アプリが完全に応答する前に、アプリが作業を実行していることを示していることです。 ゲームの場合、最初の画面では、一部の画像とサウンドをディスクからメモリに読み込む必要があります。 これらのタスクには時間がかかるため、アプリは、ゲームのテーマに関連する簡単なアニメーションを含む読み込みページを表示することで、ユーザーに通知を受け取ります。

3 番目のステージは、読み込みページを置き換える対話型 UI を作成するための最小限の情報セットがゲームに追加された後に開始されます。 この時点で、オンライン ゲームで使用できる唯一の情報は、アプリがディスクから読み込んだコンテンツです。 ゲームには対話型 UI を作成するのに十分なコンテンツを含めることができますが、オンライン ゲームであるため、インターネットに接続して追加情報をダウンロードするまでは完全には機能しません。 必要なすべての情報が得られるまで、ユーザーは UI を操作できますが、Web からの追加データを必要とする機能は、コンテンツがまだ読み込まれているというフィードバックを提供する必要があります。 アプリが完全に機能するまでに時間がかかる場合があるため、できるだけ早く機能を利用できるようにすることが重要です。

オンライン ゲームのスタートアップの 3 つの段階を特定したので、それらを実際のコードに結び付けてみましょう。

フェーズ 1 とフェーズ 2

アプリのコンストラクターは、アプリにとって重要なデータ構造を初期化する場合にのみ使用します。 OnLaunched、最初のウィンドウをすばやく作成し、軽量コンテンツを割り当て、ウィンドウをアクティブ化して、アプリがすぐにフィードバックを表示できるようにすることに重点を置いたままにします。

public partial class App : Application
{
    public static Window MainWindow { get; private set; } = null!;

    protected override void OnLaunched(LaunchActivatedEventArgs args)
    {
        base.OnLaunched(args);

        MainWindow = new MainWindow();
        MainWindow.Content = new LoadingPage();
        MainWindow.Activate();

        _ = InitializeAsync();
    }

    private async Task InitializeAsync()
    {
        // Asynchronously restore state and load the minimum data needed
        // to create the first interactive UI.
        await LoadInitialDataAsync();

        MainWindow.Content = new GameHomePage();
    }

    private static Task LoadInitialDataAsync()
    {
        // Download data to populate the initial UI.
        return Task.CompletedTask;
    }
}

OnLaunched の主要なタスクの 1 つは、UI を作成し、Window.Content に割り当ててから Window.Activate を呼び出す方法です。 複数のアクティブ化フローが必要な場合は、同じ原則を維持します。軽量コンテンツをすばやく表示し、コストのかかる作業を重要なスタートアップ パスから移動します。

起動中に読み込みページを表示するアプリは、バックグラウンドでメイン UI を作成する作業を開始できます。 その要素が作成されると、 その FrameworkElement.Loaded イベントが発生します。 イベント ハンドラーでは、現在読み込み画面であるウィンドウのコンテンツを、新しく作成したホーム ページに置き換えることができます。

初期化期間が長いアプリでは、読み込みページが表示されるのが重要です。 スタートアップ プロセスに関するフィードバックを提供する以外に、アプリが進行していることをユーザーが確認できるように、ウィンドウをすばやくアクティブ化する必要があります。

partial class GameHomePage : Page
{
    public GameHomePage()
    {
        InitializeComponent();

        // Add a handler to be called when the home page has been loaded.
        Loaded += GameHomePageLoaded;

        // Load the minimal amount of image and sound data from disk necessary
        // to create the home page.
    }

    private void GameHomePageLoaded(object sender, RoutedEventArgs e)
    {
        // Set the content of the main window to the home page now that it's
        // ready to be displayed.
        App.MainWindow.Content = this;
    }
}

フェーズ 3

アプリに UI が表示されたからといって、完全に使用できる状態であるとは限りません。 ゲームの場合、UI には、インターネットからのデータを必要とする機能のプレースホルダーが表示されます。 この時点で、ゲームはアプリを完全に機能させるために必要な追加データをダウンロードし、データの取得時に機能を段階的に有効にします。

スタートアップに必要なコンテンツの多くをアプリと共にパッケージ化できる場合があります。 これは単純なゲームの場合です。 これにより、スタートアップ プロセスが非常に簡単になります。 しかし、ニュースリーダーや写真閲覧者などの多くのプログラムは、機能するためにウェブから情報を引き出す必要があります。 このデータは大きくなる可能性があり、ダウンロードにかなりの時間がかかる場合があります。 起動時にアプリがこのデータを取得する方法は、認識されるパフォーマンスに大きな影響を与える可能性があります。

アプリが起動の最初または 2 番目のフェーズで機能に必要なデータ セット全体をダウンロードしようとした場合、読み込みページが長すぎる場合があります。 これにより、アプリが停止しているように見えます。 フェーズ 2 でプレースホルダー要素を含む対話型 UI を表示するために必要な最小限のデータをダウンロードし、フェーズ 3 でプレースホルダー要素を置き換えるデータを段階的に読み込むようお勧めします。 データの処理の詳細については、「 ListView と GridView の最適化」を参照してください。

起動時の各フェーズに対するアプリの正確な反応は完全にユーザーの自由ですが、軽量の初期 UI、読み込み画面、プログレッシブ データ読み込みを使用して可能な限り多くのフィードバックをユーザーに提供することで、アプリの操作性が向上します。

スタートアップ パス内のマネージド アセンブリを最小化する

再利用可能なコードは、多くの場合、プロジェクトに含まれるモジュール (DLL) の形式で提供されます。 これらのモジュールを読み込むにはディスクにアクセスする必要があり、コストが加算される可能性があります。 これはコールド スタートアップに最も大きな影響を与えますが、ウォーム スタートアップにも影響する可能性があります。 .NET アプリでは、CLR は必要に応じてアセンブリを読み込むことで、そのコストをできるだけ遅らせようとします。 つまり、CLR は、実行されたメソッドがモジュールを参照するまでモジュールを読み込むことはありません。 そのため、CLR が不要なモジュールを読み込めないように、スタートアップ コードでアプリの起動に必要なアセンブリのみを参照します。 不要な参照を持つ未使用のコード パスがスタートアップ パスにある場合は、不要な読み込みを回避するために、これらのコード パスを他のメソッドに移動します。

モジュールの負荷を減らすもう 1 つの方法は、アプリ モジュールを組み合わせることです。 通常、1 つの大きなアセンブリの読み込みには、2 つの小さなアセンブリを読み込むよりも時間がかかります。 これは常に可能であるとは限りません。また、開発者の生産性やコードの再利用性に大きな違いがない場合にのみ、モジュールを組み合わせる必要があります。 PerfViewWindows Performance Analyzer (WPA) などのツールを使用して、起動時に読み込まれるモジュールを確認できます。

スマート Web 要求を行う

XAML、画像、アプリにとって重要なその他のファイルなど、アプリのコンテンツをローカルにパッケージ化することで、アプリの読み込み時間を大幅に短縮できます。 ディスク操作は、ネットワーク操作よりも高速です。 初期化時にアプリで特定のファイルが必要な場合は、リモート サーバーから取得するのではなく、ディスクから読み込むことで、全体的な起動時間を短縮できます。

ジャーナリングとキャッシュを効率的に行う

Frame コントロールはナビゲーション機能を提供します。 ページへのナビゲーション (Navigate メソッド)、ナビゲーション ジャーナリング (BackStack プロパティと ForwardStack プロパティ、 GoForward メソッドと GoBack メソッド)、ページ キャッシュ (Page.NavigationCacheMode)、シリアル化のサポート (GetNavigationState メソッド) が提供されます。

Frameで注意する必要があるパフォーマンスは、主にジャーナリングとページ キャッシュに関することです。

フレーム ジャーナリングFrame.Navigateを持つページに移動すると、現在のページのPageStackEntryFrame.BackStack コレクションに追加されます。 PageStackEntry は比較的小さいですが、 BackStack コレクションのサイズに組み込みの制限はありません。 ユーザーがループ内を移動し、このコレクションを無期限に拡張できる可能性があります。

PageStackEntryには、Frame.Navigate メソッドに渡されたパラメーターも含まれています。 int メソッドを機能させるには、パラメーターをstringFrame.GetNavigationStateなどのプリミティブなシリアル化可能な型にすることをお勧めします。 ただし、そのパラメーターは、より大量のワーキング セットまたはその他のリソースを占めるオブジェクトを参照する可能性があり、 BackStack 内の各エントリのコストが高くなる可能性があります。 たとえば、 StorageFile をパラメーターとして使用する可能性があり、その結果、 BackStack はファイルの数が無限に開いたままになる可能性があります。

そのため、ナビゲーション パラメーターを小さくし、 BackStackのサイズを制限することをお勧めします。 BackStackは C# の標準コレクションであるため、エントリを削除するだけでトリミングできます。

ページ キャッシュ。 既定では、 Frame.Navigate メソッドを使用してページに移動すると、ページの新しいインスタンスがインスタンス化されます。 同様に、 Frame.GoBackを使用して前のページに戻ると、前のページの新しいインスタンスが割り当てられます。

Frame には、これらのインスタンス化を回避できるオプションのページ キャッシュも用意されています。 ページをキャッシュに格納するには、 Page.NavigationCacheMode プロパティを使用します。 このモードを Required に設定すると、ページは強制的にキャッシュされますが、 Enabled に設定するとキャッシュできるようになります。 既定では、キャッシュ サイズは 10 ページですが、これは Frame.CacheSize プロパティでオーバーライドできます。 すべての Required ページがキャッシュされ、必要なページ CacheSize 数より少ない場合は、 Enabled ページもキャッシュできます。

ページ キャッシュは、インスタンス化を回避し、ナビゲーションのパフォーマンスを向上させることで、パフォーマンスを向上させることができます。 ページ キャッシュは、オーバーキャッシュによってパフォーマンスが低下し、ワーキング セットに影響を与える可能性があります。

そのため、アプリケーションに適したページ キャッシュを使用することをお勧めします。 たとえば、 Frame内の項目の一覧を表示するアプリがあり、項目を選択すると、そのアイテムの詳細ページにフレームが移動するとします。 リスト ページはキャッシュに設定されている可能性があります。 詳細ページがすべての項目で同じ場合は、おそらくキャッシュする必要があります。 ただし、詳細ページの異種性が高い場合は、キャッシュをオフのままにすることをお勧めします。