概要
WPF で Label を大量に配置した画面において、初期表示のレイアウトに時間がかかる事象を確認した。
本記事では、Label と TextBlock の構造差に基づいて原因を整理し、性能を優先する画面での実装方針を示す。
結論を先に述べる。
Label と TextBlock の差は実測できるが、それが問題になるのは UI 仮想化が効いていない画面に限られる。
仮想化が有効な ListBox に 10,000 件を流した場合、両者のレイアウト時間に実用上の差は残らない。
コントロールの置き換えは、仮想化を確認したうえで検討する順序が適切である。
前提・対象環境
- フレームワーク/言語: .NET 10 / C# / WPF
- 対象コントロール:
Label、TextBlock、ContentPresenter - アーキテクチャ: MVVM(コードビハインドでも同様)
- 想定画面: 一覧・ダッシュボードなどテキスト要素を多数表示する画面
- 計測環境: Windows 11、既定テーマ(Aero2)、表示スケール 100%
本記事の数値は、上記環境で実際にアプリケーションを起動し、Measure から UpdateLayout までの所要時間を計測して得たものである。
各条件を交互に 15 回試行し、その最小値を採っている。
所要時間は実行環境に依存するため、絶対値ではなく条件間の比率として読む。
問題
Label を数十個から数百個単位で配置した画面では、以下の問題が発生しやすい。
- 初期表示が遅くなる。
- 同じ文字列表示用途でも
TextBlock構成よりメモリ使用量が増える。
フォーム見出し向けに Label を使う実装をそのまま一覧表示へ展開すると、表示性能が劣化しやすい。
原因・背景
TextBlock はテキスト描画を主目的とする軽量な要素であり、FrameworkElement を直接継承する。
一方で Label は ContentControl を継承し、文字列以外のコンテンツも扱える汎用 UI 部品として設計されている。
Label は内部で ContentPresenter を介して描画を行い、必要に応じてアクセスキー処理や Target 連携などの機能を提供する。
このため、単純な文字表示だけを大量に行う用途では、TextBlock よりもオーバーヘッドが大きくなる。
visual ツリーの内訳
差の実体は、1 個あたりに構築される visual の数である。
既定テンプレートで文字列を与えたときの構成は次のとおりになる。
| 要素 | 構築される visual ツリー | visual 数 |
|---|---|---|
TextBlock |
TextBlock |
1 |
ContentPresenter |
ContentPresenter → TextBlock |
2 |
Label |
Label → Border → ContentPresenter → TextBlock |
4 |
Label 1 個につき、Border・ContentPresenter・実際に文字を描く TextBlock が追加で構築される。
測定・配置で処理される visual も 4 倍になる。
非仮想化時の実測
同じ文字列を StackPanel に並べ、レイアウト完了までの時間と生成された visual の総数を測ると次のようになる。
| 項目数 | Label の visual 数 | Label(ms) | TextBlock の visual 数 | TextBlock(ms) |
|---|---|---|---|---|
| 250 | 1,000 | 106 | 250 | 56 |
| 1,000 | 4,000 | 257 | 1,000 | 137 |
| 4,000 | 16,000 | 1,065 | 4,000 | 537 |
.NET 10 / Windows 11、既定テーマ(Aero2)の ControlTemplate で、Content にアクセスキーを含まない文字列を与えた場合の実測値。仮想化されない StackPanel に並べている。visual 数はテーマや ControlTemplate の差し替えで変わり、所要時間は実行環境に依存するため、絶対値ではなく比率を目安として読む。
visual 数は要素数に正確に比例し、Label は常に TextBlock の 4 倍になる。
一方でレイアウト時間の差はおよそ 2 倍にとどまり、visual 数の比とは一致しない。
visual の数だけで所要時間が決まるわけではない。
メモリも測った。
1,000 個を並べてレイアウトしたあと、GC を掛けても残るマネージドヒープの量は、Label が TextBlock の約 2.6 倍だった。
アンダーバーを含む文字列を与えた Label は約 5.1 倍になる。
| KB | TextBlock との比 | |
|---|---|---|
| Label | 4,111 | 2.6x |
| Label(Content に '_' を含む) | 8,135 | 5.1x |
| TextBlock | 1,588 | 1.0x |
同一環境で、StackPanel に 1,000 個を並べてレイアウトし、GC.GetTotalMemory(true) の前後差を 5 回測った中央値。
アンダーバーを含む文字列による追加コスト
Label のコストは Content の内容によっても変わる。
文字列にアンダーバー(_)が含まれると、ContentPresenter は TextBlock ではなく AccessText を生成し、visual が 1 段増えて 5 個になる。
1,000 個で固定し、構成を変えて比較した結果が次の表である。
| visual 数 | レイアウト(ms) | |
|---|---|---|
| Label | 4,000 | 199 |
| Label(Content に '_' を含む) | 5,000 | 694 |
| Label + ContentTemplate | 4,000 | 249 |
| Label + ContentTemplate、'_' | 4,000 | 243 |
| ContentPresenter | 2,000 | 147 |
| AccessText('_') | 2,000 | 234 |
| TextBlock | 1,000 | 129 |
同一環境で 1,000 個を StackPanel に並べた場合の実測値。Content にアンダーバーを含む Label は AccessText が挟まって visual がアンダーバーなしより 1 個多くなり、レイアウト時間は約 3.5 倍だった。この増分のうち、AccessText 自体と visual の増加がそれぞれどれだけかは測っていない。ContentTemplate に TextBlock を指定した Label は、アンダーバーを含む文字列を与えてもレイアウト時間が増えない。AccessText 単体の行は比較のため。
visual 数は 4 個から 5 個へ 25% 増えるだけだが、レイアウト時間は 199 ms から 694 ms へ、約 3.5 倍に達する。
AccessText 単体でも TextBlock 単体の約 1.8 倍(129 ms に対して 234 ms)かかった。
ただし Label に挟まったときの増分(約 500 ms)は単体どうしの差(約 100 ms)より大きく、AccessText 自体の重さだけでは説明できない。増分の内訳は測っていない。
ContentTemplate に TextBlock を指定した Label は、アンダーバーを含む文字列を与えても AccessText が挟まらず、visual は 4 個のままだった。
レイアウト時間は 243 ms で、アンダーバーを含まない文字列の場合(249 ms)と変わらない。
この差は、ファイルパスや識別子のようにアンダーバーを含みやすいデータを表示する画面で実際に効く。
アンダーバーが表示から消える問題そのものは「WPF の Label でアンダーバーが消える理由と回避方法」で扱う。
仮想化を有効にした場合
ここまでは StackPanel に直接並べた、仮想化されない構成での比較である。
UI 仮想化が有効な ItemsControl では前提が変わる。
画面に見えている範囲と、その前後の VirtualizingPanel.CacheLength で決まるキャッシュ領域のコンテナだけが実体化されるため、コレクションの件数がいくら多くても、同時に存在する visual の数は一定に保たれる。
仮想化を有効にした ListBox に 10,000 件を流し、ItemTemplate の中身だけを入れ替えて比較した結果が次の表である。
| visual 数 | レイアウト(ms) | |
|---|---|---|
| Label | 238 | 16 |
| TextBlock | 148 | 13 |
同一環境で、IsVirtualizing="True"・VirtualizationMode="Recycling" の ListBox に 10,000 件をバインドした場合の実測値。実体化されるコンテナは、表示範囲と、その前後の VirtualizingPanel.CacheLength で決まるキャッシュ領域の分だけであるため、visual の総数は件数に依存しない。レイアウト時間の差は、同じ条件で繰り返した試行どうしの差より小さく、非仮想化時に見えた約 2 倍の差は残らない。
非仮想化の StackPanel で 4,000 個を並べた場合と比べると、件数が 2.5 倍でありながらレイアウト時間は 2 桁小さい。
Label と TextBlock の差は、同じ条件で繰り返した試行どうしの差より小さい。
15 回の試行の中央値の差は約 4 ms だが、Label だけを見ても最小 15.5 ms から最大 114.5 ms まで開いた。
| 最小 | 中央値 | 最大 | |
|---|---|---|---|
| StackPanel 1,000、Label | 256.5 | 273.8 | 420.9 |
| StackPanel 1,000、TextBlock | 136.7 | 151.1 | 213.4 |
| ListBox 10,000、Label | 15.5 | 20.5 | 114.5 |
| ListBox 10,000、TextBlock | 12.8 | 16.4 | 75.6 |
上の 2 つの表を作った 15 回の試行の分布。表の値は最小値である。
「Label を大量配置すると遅い」という現象の主因は、Label そのものではなく仮想化が効いていないことにある。
コントロールの選択で得られるのは約 2 倍の改善だが、仮想化の有無による差はそれよりはるかに大きい。
改善の順序
改善は次の順序で検討する。
- 仮想化が効いているかを先に確認する。 一覧表示であれば、
ItemsControl系のコントロールを使い、UI 仮想化を有効に保つ。ここが崩れていると、コントロールを置き換えても効果は限定的である。 - 表示専用テキストを
TextBlockに寄せる。 仮想化が使えない画面や、要素数が固定で多い画面で有効である。 - アンダーバーを含むデータには
ContentTemplateを使う。Labelを維持しつつAccessTextの追加コストを避けられる。
用途の分離としては次のとおりである。
- 表示専用テキスト: 原則
TextBlockを採用する。 - 入力フォーム見出し:
Targetやアクセスキーが必要な箇所のみLabelを使用する。 - 既存画面の改修:
Labelを一括で置換せず、要件を満たす範囲で段階的にTextBlock化する。
段階 1: 仮想化を保つ
一覧表示では、ItemsControl をそのまま使うのではなく、仮想化パネルを持つコントロールを選ぶ。
ItemsControl の既定の ItemsPanel は StackPanel であり、仮想化されない。
<!-- 仮想化されない。件数に比例してコンテナが作られる -->
<ItemsControl ItemsSource="{Binding Items}" />
<!-- 仮想化される。ListBox の既定は VirtualizingStackPanel -->
<ListBox ItemsSource="{Binding Items}"
ScrollViewer.CanContentScroll="True"
VirtualizingPanel.IsVirtualizing="True"
VirtualizingPanel.VirtualizationMode="Recycling" />
ItemsControl のまま仮想化したい場合は、ItemsPanel に VirtualizingStackPanel を指定し、ScrollViewer を伴うテンプレートを与える必要がある。
選択機能が不要でも ListBox を使い、Focusable や選択の見た目をスタイルで無効化するほうが確実である。
ScrollViewer.CanContentScroll を False にすると、ピクセル単位でスクロールするようになる代わりに、仮想化が効かなくなる点に注意する。
2,000 件の ListBox では、既定では 23 個だった ListBoxItem が、2,000 個すべて実体化された。
CanContentScroll の既定値は False だが、ListBox の既定スタイルが True を与えている。
滑らかなスクロールが目的なら、仮想化を保ったまま VirtualizingPanel.ScrollUnit="Pixel" を指定すればよい。実体化される ListBoxItem は 23 個のままで、スクロールの単位だけがピクセルに変わった(ExtentHeight が件数の 2,000 から 39,920 ピクセルに変わる)。
| 計測値 | |
|---|---|
| ScrollViewer.CanContentScroll の既定値 | False |
| ListBox(既定のスタイル) | CanContentScroll True、ExtentHeight 2,000、実体化した項目 23 |
| ScrollUnit="Pixel" | CanContentScroll True、ExtentHeight 39,920、実体化した項目 23 |
| CanContentScroll="False" | CanContentScroll False、ExtentHeight 39,920、実体化した項目 2,000 |
.NET 10 / Windows 11 で、2,000 件を持つ ListBox を表示し、中ほどまでスクロールしてから ListBoxItem を数えた結果。ExtentHeight は、項目単位のスクロールなら件数、ピクセル単位ならピクセル数になる。
段階 2: 表示専用の Label を TextBlock に置き換える
単純表示用途の Label を TextBlock に置き換える。
この置き換えは、見た目を維持しつつ描画コストを削減する目的で実施する。
<!-- 置き換え前 -->
<Label Content="ステータス: 実行中" />
<!-- 置き換え後 -->
<TextBlock Text="ステータス: 実行中" />
この変更により、表示専用箇所から Label 固有機能を外し、軽量な描画構成に統一できる。
ただし Label の既定 Padding がなくなるため、必要に応じて Margin や Padding を明示する。
段階 3: アンダーバーを含むデータに ContentTemplate を使う
Label の外観を保ったまま AccessText を避けるには、ContentTemplate に TextBlock を指定する。
<Label Content="{Binding FilePath}">
<Label.ContentTemplate>
<DataTemplate>
<TextBlock Text="{Binding}" />
</DataTemplate>
</Label.ContentTemplate>
</Label>
ContentPresenter が AccessText を生成しなくなるため、アンダーバーがそのまま表示され、レイアウト時間もアンダーバーを含まない文字列の場合と変わらなくなる。
ただし ContentTemplate は Content が文字列以外のオブジェクトである場合にも適用されるため、対象は文字列表示に限定する。
Label を残す箇所
Label が必要な入力フォーム見出しの例を示す。
このパターンでは操作性とアクセシビリティを維持するため、Label を継続利用する。
<StackPanel Orientation="Horizontal">
<Label Content="名前(_N):"
Target="{Binding ElementName=NameTextBox}"
VerticalAlignment="Center" />
<TextBox x:Name="NameTextBox" Width="180" />
</StackPanel>
Alt + N によるフォーカス移動と、ラベルクリック時のフォーカス移動を利用する要件では Label が適する。
このため、性能改善では「全面禁止」ではなく「要件ベースの使い分け」が必要となる。
なお、この例のようにアクセスキーを意図して使う場合は AccessText が構築されるが、フォーム見出しの個数は通常わずかであり、コストは問題にならない。
AccessText のコストが効くのは、アンダーバーを含むデータを大量に表示する場合である。
注意点
LabelからTextBlockへ置き換えると、アクセスキー(_)とTargetによるフォーカス連携は失われる。- 長文表示では、
TextBlock側でTextWrapping="Wrap"やTextTrimmingを明示しないと意図した表示にならない場合がある。 - 既存 UI では
Label既定余白に依存していることがあるため、置き換え後の行間と整列を確認する必要がある。 DataGridやItemsControlのテンプレート内で多数描画する場合は、コントロールの選択よりも先に仮想化設定を評価する。仮想化が無効なままTextBlockに置き換えても、得られる改善は限定的である。- 本記事の計測は
MeasureからUpdateLayoutまでのレイアウト時間である。実際の描画(レンダリング)やスクロール時の再利用コストは含まない。スクロールの滑らかさを評価する場合は、別途スクロール操作を伴う計測が必要になる。 - 数十個規模であれば、
LabelとTextBlockの差は体感できない。置き換えは、実際に遅い画面を特定してから行う。
代替案・比較
| 方法 | メリット | デメリット | 適するケース |
|---|---|---|---|
| 仮想化を有効にする | 件数に依存しないコストにできる。効果が最も大きい | 既定ではスクロールが項目単位になる(ScrollUnit="Pixel" でピクセル単位にできる)。CanContentScroll="False" と併用できない |
一覧・グリッドなど件数が可変の画面 |
すべて Label のまま維持 |
アクセスキーや Target を既存仕様どおり保持できる |
大量表示時の描画コストが高い | 入力フォーム中心で要素数が少ない画面 |
表示専用箇所を TextBlock へ置換 |
描画とメモリ負荷を抑えやすい | 余白・折り返し設定の見直しが必要 | 仮想化できない、要素数の多い固定レイアウト |
Label + ContentTemplate |
外観を保ったまま AccessText のコストを回避できる |
XAML 量が増える | アンダーバーを含むデータを Label で表示する画面 |
画面全体を TextBlock 化 |
単純化しやすく軽量 | Label 固有の操作性を失う |
入力連携が不要な読み取り専用画面 |
まとめ
WPF で Label を大量配置した場合の遅延要因は、ContentControl としての汎用機能に起因する描画オーバーヘッドである。
Label は 1 個あたり 4 個の visual を構築し、TextBlock の 4 倍になる。
非仮想化の構成では、レイアウト時間で約 2 倍、GC を掛けても残るマネージドヒープの量で約 2.6 倍の差が出る(GC.GetTotalMemory(true) による値で、メモリの総使用量ではない)。
ただし実務上の優先順位は次のとおりである。
- まず仮想化を確認する。 仮想化が有効なら、
LabelとTextBlockのレイアウト時間に実用上の差は残らない。コントロールの置き換えより効果が大きい。 - 仮想化できない画面では
TextBlockに寄せる。 要素数が固定で多い画面では、コントロール選択が効く。 - アンダーバーを含むデータを
Labelで表示する場合はContentTemplateを使う。AccessTextが挟まると、visual が 1 個多くなり、レイアウト時間は約 3.5 倍だった。 Targetとアクセスキーが必要な箇所はLabelを維持する。 フォーム見出し程度の個数であればコストは問題にならない。
「Label は遅いから使わない」ではなく、「仮想化が効いているか」「その画面で本当に大量に描画しているか」を先に確認したうえで、要件に応じて選ぶ。
- WPF の Label でアンダーバーが消える理由と回避方法
- WPF ListBox 仮想化環境での SelectedItems が消えたように見える問題とその解決法
- Label(WPF 標準コントロールデモアプリ):Label の既定値と、アクセスキーがフォーカスをどこへ移すかを実測したページ
- TextBlock(WPF 標準コントロールデモアプリ):
TextとInlinesの関係と、テンプレートを持たない TextBlock の折り返しと省略を実測したページ