XAML マークアップの最適化
UI が複雑な場合は、XAML マークアップを解析し、メモリ内にオブジェクトを構築するのに時間がかかります。 次に、アプリの XAML マークアップの解析および読み込み時間や、メモリ効率の向上に役立つヒントを紹介します。
アプリの起動時に読み込まれる XAML マークアップを、最初の UI に必要な分だけに制限します。 最初のページ (ページ リソースを含む) のマークアップを調べて、すぐには必要とされない余分な要素を読み込んでいないことを確認します。 このような要素は、リソース ディクショナリ、初期状態で折りたたまれている要素、別の要素の上に描画される要素などの、さまざまなソースに由来する可能性があります。
効率を求めて XAML を最適化するには、トレードオフが必要になります。すべての状況に 1 つの解決策があるとは限りません。 ここでは、いくつかの一般的な問題を取り上げながら、アプリに適したトレードオフを実践するためのガイドラインを示します。
要素数の最小化
XAML プラットフォームでは大量の要素を表示できますが、目的のビジュアルを満たすためのページ上の要素の数を減らすと、アプリ レイアウトの作成とレンダリングをより速く行うことができます。
UI コントロールのレイアウトは、アプリの起動時に作成される UI 要素の数に影響を及ぼします。 レイアウトの最適化について詳しくは、「XAML レイアウトの最適化」をご覧ください。
データ テンプレートでは、データ項目ごとに各要素が繰り返し作成されるため、要素数が非常に重要になります。 リストやグリッドで要素数を減らす方法については、「ListView と GridView の UI の最適化」記事の「項目ごとの要素の削減」をご覧ください。
ここでは、アプリの起動時に読み込む必要のある要素数を減らすための方法をいくつか見ていきます。
項目の作成を延期する
すぐには表示されない要素が XAML マークアップに含まれている場合は、それらの要素が表示されるまで読み込みを延期できます。 たとえば、タブのような UI では、セカンダリ タブなどの非表示のコンテンツの作成を遅らせることができます。 また、既定では項目をグリッド ビューで表示し、データを表示するときにリスト表示に切り替えるオプションをユーザーに提供する方法があります。 これにより、必要になるまでリストの読み込みを遅らせることができます。
要素が表示されるタイミングを制御するときに、Visibility プロパティの代わりに x:Load attribute を使用します。 要素の Visibility が Collapsed に設定されているときは、その要素がレンダリング パスでスキップされますが、オブジェクト インスタンスのメモリ使用のコストは発生します。 代わりに x:Load を使用すると、オブジェクト インスタンスは必要になるまで作成されないため、メモリ コストはさらに低くなります。 欠点は、UI が読み込まれていないときに、小さなメモリ オーバーヘッド (約 600 バイト) が発生することです。
Note
要素の遅延読み込みには、x:Load または x:DeferLoadStrategy 属性を使用できます。 x:Load 属性は、Windows 10 Creators Update (Version 1703、SDK ビルド 15063) 以降で使用できます。 x:Load を使用するには、Visual Studio プロジェクトの対象とする最小バージョンを Windows 10 Creators Update (10.0、ビルド 15063) にする必要があります。 それより前のバージョンを対象とする場合は、x:DeferLoadStrategy を使用します。
以降の例では、異なる方法を使用して UI 要素を非表示にしたときの要素数とメモリ使用量の違いを示します。 ページのルート Grid に、まったく同じ項目を含む ListView と GridView が配置されています。 ListView は非表示ですが、GridView は表示されています。 これらの例の各 XAML が、画面上に同一の UI を生成します。 要素数とメモリ使用量の確認には、Visual Studio のプロファイリングとパフォーマンスに関するツールを使用します。
オプション 1 - 非効率的
この例では、ListView は読み込まれますが、Width が 0 なので表示されません。 ListView とその各子要素は、ビジュアル ツリー内に作成され、メモリに読み込まれます。
<!-- NOTE: EXAMPLE OF INEFFICIENT CODE; DO NOT COPY-PASTE.-->
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<ListView x:Name="List1" Width="0">
<ListViewItem>Item 1</ListViewItem>
<ListViewItem>Item 2</ListViewItem>
<ListViewItem>Item 3</ListViewItem>
<ListViewItem>Item 4</ListViewItem>
<ListViewItem>Item 5</ListViewItem>
<ListViewItem>Item 6</ListViewItem>
<ListViewItem>Item 7</ListViewItem>
<ListViewItem>Item 8</ListViewItem>
<ListViewItem>Item 9</ListViewItem>
<ListViewItem>Item 10</ListViewItem>
</ListView>
<GridView x:Name="Grid1">
<GridViewItem>Item 1</GridViewItem>
<GridViewItem>Item 2</GridViewItem>
<GridViewItem>Item 3</GridViewItem>
<GridViewItem>Item 4</GridViewItem>
<GridViewItem>Item 5</GridViewItem>
<GridViewItem>Item 6</GridViewItem>
<GridViewItem>Item 7</GridViewItem>
<GridViewItem>Item 8</GridViewItem>
<GridViewItem>Item 9</GridViewItem>
<GridViewItem>Item 10</GridViewItem>
</GridView>
</Grid>
ListView を読み込んだライブ ビジュアル ツリー。 ページの要素の総数は 89 です。
ListView とその子がメモリに読み込まれています。
オプション 2 - やや効率的
この例では、ListView の Visibility が Collapsed に設定されています (その他の XAML は元と同じです)。 ListView はビジュアル ツリーに作成されますが、その子要素は作成されません。 ただし、メモリには子要素も読み込まれるため、メモリ使用量は前の例と同じです。
<ListView x:Name="List1" Visibility="Collapsed">
ListView を Collapsed に設定したライブ ビジュアル ツリー。 ページの要素の総数は 46 です。
ListView とその子がメモリに読み込まれています。
オプション 3 - 最も効率的
この例では、ListView の x:Load 属性が False に設定されています (その他の XAML は元と同じです)。 この ListView は、起動時にはビジュアル ツリーに作成されず、メモリにも読み込まれません。
<ListView x:Name="List1" Visibility="Collapsed" x:Load="False">
ListView が読み込まれていないライブ ビジュアル ツリー。 ページの要素の総数は 45 です。
ListView とその子はメモリに読み込まれていません。
Note
これらの例の要素数とメモリ使用量は非常に小さく、概念を紹介するためだけに示したものです。 これらの例では、x:Load を使用する場合のオーバーヘッドがメモリの節約量よりも大きいため、アプリにとってのメリットはありません。 アプリにとって遅延読み込みが効果的かどうかを判断するには、アプリに対してプロファイリング ツールを使用する必要があります。
レイアウト パネルのプロパティを使用する
レイアウト パネルには Background プロパティが用意されているため、色を付ける目的でパネルの前面に Rectangle を配置する必要はありません。
非効率的
<!-- NOTE: EXAMPLE OF INEFFICIENT CODE; DO NOT COPY-PASTE. -->
<Grid>
<Rectangle Fill="Black"/>
</Grid>
効率的
<Grid Background="Black"/>
レイアウト パネルには組み込みの境界線プロパティも用意されているため、レイアウト パネルの周りに Border 要素を追加する必要はありません。 詳細と例については、「XAML レイアウトの最適化」をご覧ください。
ベクター ベースの要素の代わりに画像を使用する
同じベクター ベースの要素を十分な回数、再利用する場合は、代わりに Image 要素を使用すると効率が高まります。 ベクター ベースの要素は、CPU が個々の要素をそれぞれ個別に作成する必要があるため、負荷が高くなる可能性があります。 これに対して画像ファイルは、1 回デコードするだけで済みます。
リソースとリソース ディクショナリの最適化
通常、アプリ内の複数の場所から参照されるリソースをある程度グローバルなレベルに格納するには、リソース ディクショナリを使用します。 たとえば、スタイル、ブラシ、テンプレートなどです。
一般に、ResourceDictionary は、要求されない限りリソースをインスタンス化しないように最適化されています。 ただし、リソースが不要にインスタンス化されないようにするために、避けた方がよい状況もあります。
x:Name を含むリソース
リソースを参照するには、x:Key 属性を使用します。 x:Name 属性を持つリソースは、プラットフォームの最適化のメリットが適用されず、ResourceDictionary が作成されるとすぐにインスタンス化されます。 このようになるのは、アプリがこのリソースへのフィールド アクセスを必要としていることを x:Name がプラットフォームに伝達し、プラットフォームが参照を作成する何かを作成する必要が生じるためです。
UserControl 内の ResourceDictionary
UserControl の内部に定義された ResourceDictionary にはペナルティが発生します。 プラットフォームは、UserControl のすべてのインスタンスに対して、このような ResourceDictionary のコピーを作成します。 よく使われる UserControl を使っている場合、UserControl から ResourceDictionary を移動し、ページ レベルに配置します。
リソースと ResourceDictionary のスコープ
ページで参照しているユーザー コントロールやリソースが別のファイルに定義されている場合は、そのファイルもフレームワークが解析します。
次の例では、InitialPage.xaml が ExampleResourceDictionary.xaml のリソースを 1 つ使用するため、起動時に ExampleResourceDictionary.xaml 全体を解析する必要があります。
InitialPage.xaml
<Page x:Class="ExampleNamespace.InitialPage" ...>
<Page.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="ExampleResourceDictionary.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Page.Resources>
<Grid>
<TextBox Foreground="{StaticResource TextBrush}"/>
</Grid>
</Page>
ExampleResourceDictionary.xaml
<ResourceDictionary>
<SolidColorBrush x:Key="TextBrush" Color="#FF3F42CC"/>
<!--This ResourceDictionary contains many other resources that
are used in the app, but are not needed during startup.-->
</ResourceDictionary>
1 つのリソースをアプリ全体の多くのページで使用する場合、そのリソースを App.xaml に格納することをお勧めします。これにより、重複を避けることができます。 ただし、App.xaml はアプリの起動時に解析されるため、1 つのページでのみ使用されるリソースは、(そのページが最初のページでない限り) ページのローカル リソースに配置する必要があります。 次の例は、(最初のページ以外の) 1 つのページでのみ使用されるリソースを含む App.xaml を示しています。 この場合、アプリの起動時間が不必要に増加します。
App.xaml
<!-- NOTE: EXAMPLE OF INEFFICIENT CODE; DO NOT COPY-PASTE. -->
<Application ...>
<Application.Resources>
<SolidColorBrush x:Key="DefaultAppTextBrush" Color="#FF3F42CC"/>
<SolidColorBrush x:Key="InitialPageTextBrush" Color="#FF3F42CC"/>
<SolidColorBrush x:Key="SecondPageTextBrush" Color="#FF3F42CC"/>
<SolidColorBrush x:Key="ThirdPageTextBrush" Color="#FF3F42CC"/>
</Application.Resources>
</Application>
InitialPage.xaml
<!-- NOTE: EXAMPLE OF INEFFICIENT CODE; DO NOT COPY-PASTE. -->
<Page x:Class="ExampleNamespace.InitialPage" ...>
<StackPanel>
<TextBox Foreground="{StaticResource InitialPageTextBrush}"/>
</StackPanel>
</Page>
SecondPage.xaml
<!-- NOTE: EXAMPLE OF INEFFICIENT CODE; DO NOT COPY-PASTE. -->
<Page x:Class="ExampleNamespace.SecondPage" ...>
<StackPanel>
<Button Content="Submit" Foreground="{StaticResource SecondPageTextBrush}"/>
</StackPanel>
</Page>
この例の効率を改善するには、SecondPageTextBrush
を SecondPage.xaml に、ThirdPageTextBrush
を ThirdPage.xaml に移動します。 InitialPageTextBrush
は、どの場合でもアプリケーション リソースをアプリの起動時に解析する必要があるため、App.xaml 内に残しておくことができます。
同じように見える複数のブラシを 1 つのリソースに統合する
XAML プラットフォームは、よく使用されるオブジェクトをキャッシュして、できるだけ多く再利用できるようにします。 しかし XAML は、あるマークアップで宣言されているブラシが別のマークアップで宣言されているブラシと同じであるかどうかを簡単に判断できません。 ここでは、例として SolidColorBrush を使用していますが、より多く使用される可能性があって重要なのは GradientBrush です。 事前定義された色を使用するブラシもチェックしてください。たとえば、"Orange"
と "#FFFFA500"
は同じ色です。
非効率的
<!-- NOTE: EXAMPLE OF INEFFICIENT CODE; DO NOT COPY-PASTE. -->
<Page ... >
<StackPanel>
<TextBlock>
<TextBlock.Foreground>
<SolidColorBrush Color="#FFFFA500"/>
</TextBlock.Foreground>
</TextBlock>
<Button Content="Submit">
<Button.Foreground>
<SolidColorBrush Color="#FFFFA500"/>
</Button.Foreground>
</Button>
</StackPanel>
</Page>
重複を修正するには、ブラシをリソースとして定義します。 他のページのコントロールで同じブラシを使用する場合は、それを App.xaml に移動します。
効率的
<Page ... >
<Page.Resources>
<SolidColorBrush x:Key="BrandBrush" Color="#FFFFA500"/>
</Page.Resources>
<StackPanel>
<TextBlock Foreground="{StaticResource BrandBrush}" />
<Button Content="Submit" Foreground="{StaticResource BrandBrush}" />
</StackPanel>
</Page>
過剰な描画の最小化
過剰な描画は、画面上の同じピクセルに複数のオブジェクトが描画される場合に発生します。 ただし、このガイダンスと要素数を最小限に抑えたいという要求は、トレードオフになることがあります。
視覚的な診断には、DebugSettings.IsOverdrawHeatMapEnabled を使用します。 シーンに存在するとは思わなかったオブジェクトが描画されていることに気づく場合があります。
透明または非表示の要素
要素が透明であるか、他の要素の背後に隠れているために表示されず、レイアウトに関与していない場合は、それを削除することをお勧めします。 初期の表示状態では要素が表示されないものの、他の表示状態で表示される場合は、x:Load を使ってその状態を制御するか、要素自体の Visibility を Collapsed に設定しておいて、該当の状態になったら値を Visible に変更します。 このヒューリスティックには例外があります。一般に、大多数の表示状態でプロパティに設定される値は、要素にローカルに設定するのが最良です。
複合要素
複数の要素を重ねて効果を作成する代わりに、複合要素を使います。 次の例では、結果は 2 色の図形になり、上半分は黒 (Grid の背景)、下半分は灰色 (Grid の黒い背景の上にアルファ ブレンドされた半透明の白い Rectangle) で表示されます。 この場合、結果を得るために必要なピクセルの 150% が塗りつぶされることになります。
非効率的
<!-- NOTE: EXAMPLE OF INEFFICIENT CODE; DO NOT COPY-PASTE. -->
<Grid Background="Black">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Rectangle Grid.Row="1" Fill="White" Opacity=".5"/>
</Grid>
効率的
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Rectangle Fill="Black"/>
<Rectangle Grid.Row="1" Fill="#FF7F7F7F"/>
</Grid>
レイアウト パネル
レイアウト パネルには、領域の色指定と、子要素のレイアウトの 2 つの目的があります。 さらに背後にある z オーダーの要素の領域が色付けされている場合は、前面のレイアウト パネルでその領域を塗りつぶす必要はありません。代わりに、その子のレイアウトのみに集中できます。 次に例を示します。
非効率的
<!-- NOTE: EXAMPLE OF INEFFICIENT CODE; DO NOT COPY-PASTE. -->
<GridView Background="Blue">
<GridView.ItemTemplate>
<DataTemplate>
<Grid Background="Blue"/>
</DataTemplate>
</GridView.ItemTemplate>
</GridView>
効率的
<GridView Background="Blue">
<GridView.ItemTemplate>
<DataTemplate>
<Grid/>
</DataTemplate>
</GridView.ItemTemplate>
</GridView>
Grid のヒット テストを可能にするには、その背景の値を透明に設定します。
罫線
オブジェクトの周りに境界線を描画するには、Border 要素を使用します。 次の例では、TextBox を囲む間に合わせの境界線として Grid を使用しています。 しかし、中央のセル内のすべてのピクセルが複数回描画されます。
非効率的
<!-- NOTE: EXAMPLE OF INEFFICIENT CODE; DO NOT COPY-PASTE. -->
<Grid Background="Blue" Width="300" Height="45">
<Grid.RowDefinitions>
<RowDefinition Height="5"/>
<RowDefinition/>
<RowDefinition Height="5"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="5"/>
<ColumnDefinition/>
<ColumnDefinition Width="5"/>
</Grid.ColumnDefinitions>
<TextBox Grid.Row="1" Grid.Column="1"></TextBox>
</Grid>
効率的
<Border BorderBrush="Blue" BorderThickness="5" Width="300" Height="45">
<TextBox/>
</Border>
余白
余白に注意してください。 負の余白が別のレンダリング境界に達すると、隣り合った 2 つの要素が (おそらく意図せずに) 重なり合い、過剰な描画を引き起こします。
静的コンテンツのキャッシュ
過剰な描画の別の原因として、多数の要素を重ね合わせて作成される図形があります。 複合図形を含む UIElement で CacheMode を BitmapCache に設定すると、プラットフォームが要素をいったんビットマップにレンダリングし、過剰な描画の代わりに、そのビットマップを各フレームで使用します。
非効率的
<Canvas Background="White">
<Ellipse Height="40" Width="40" Fill="Blue"/>
<Ellipse Canvas.Left="21" Height="40" Width="40" Fill="Blue"/>
<Ellipse Canvas.Top="13" Canvas.Left="10" Height="40" Width="40" Fill="Blue"/>
</Canvas>
上の図がその結果ですが、過剰に描画された領域のマップは次のようになります。 赤色が濃いほど、描画回数が多いことを示しています。
効率的
<Canvas Background="White" CacheMode="BitmapCache">
<Ellipse Height="40" Width="40" Fill="Blue"/>
<Ellipse Canvas.Left="21" Height="40" Width="40" Fill="Blue"/>
<Ellipse Canvas.Top="13" Canvas.Left="10" Height="40" Width="40" Fill="Blue"/>
</Canvas>
CacheMode の使用に注意してください。 サブ図形がアニメーション化されている場合は、この方法を使用しないでください。その目的に反し、各フレームでビットマップ キャッシュを再生成することが必要になる可能性があるためです。
XBF2 の使用
XBF2 は、実行時にすべてのテキスト解析コストを回避する XAML マークアップのバイナリ表現です。 読み込みとツリーの作成のためにバイナリの最適化も行い、VSM、ResourceDictionary、Styles などのヒープやオブジェクトの作成コストを改善するために XAML 型の "高速パス" を使用できるようにします。 これは完全にメモリ マッピングされるため、XAML ページの読み込みや読み取りにヒープは使用されません。 さらに、appx に格納される XAML ページのディスク使用量を削減します。 XBF2 はよりコンパクトな表現であり、同等の XAML/XBF1 ファイルのディスク使用量を最大 50% 削減できます。 たとえば、組み込みのフォト アプリの場合、XBF2 への変換によって、およそ 1 MB の XBF1 アセットが 400 KB の XBF2 アセットに縮小され、約 60% の削減が実現されました。 CPU で 15 ~ 20%、Win32 ヒープで 10 ~ 15% のメリットもアプリにもたらします。
フレームワークが提供する XAML の組み込みのコントロールとディクショナリは、既に XBF2 に完全に対応しています。 独自のアプリでは、プロジェクト ファイルで TargetPlatformVersion 8.2 以降を宣言していることを確認します。
XBF2 があるかどうかを確認するには、バイナリ エディターでアプリを開きます。XBF2 がある場合、12 番目と 13 番目のバイトは 00 02 になります。