Why WPF Slows Down with Many Labels and When to Switch to TextBlock

Why many WPF Labels slow rendering, from measured visual trees: the gap against TextBlock, underscore costs, and whether it survives virtualization.

Overview

Initial layout takes longer on screens that place a large number of WPF Label controls.
This article analyzes the cause based on the structural difference between Label and TextBlock, and presents an implementation policy for performance-sensitive screens.

The conclusion first.
The gap between Label and TextBlock is measurable, but it only matters on screens where UI virtualization is not in effect.
With 10,000 items bound to a virtualized ListBox, no practical difference in layout time remains between the two.
Replacing controls is worth considering only after virtualization has been confirmed.


Prerequisites / Environment

The values in this article were obtained by running an actual application in the environment above and timing the interval from Measure to UpdateLayout.
Each condition was run 15 times in alternation, and the minimum was taken.
Elapsed time depends on the execution environment, so read the values as ratios between conditions rather than as absolute numbers.


Problem

Screens that place dozens to hundreds of Label controls tend to exhibit the following:

Carrying an implementation that uses Label for form captions into a list display is a common way to degrade rendering performance.


Cause / Background

TextBlock is a lightweight element whose primary purpose is text rendering, and it derives directly from FrameworkElement.
Label, by contrast, derives from ContentControl and is designed as a general-purpose UI part capable of hosting content other than strings.

Label renders through a ContentPresenter and provides access-key handling and Target integration as needed.
For workloads that display large amounts of plain text, this overhead exceeds that of TextBlock.

Visual Tree Composition

The substance of the difference is the number of visuals constructed per element.
With the default template and a string content, the composition is as follows.

Element Visual tree that gets built Visuals
TextBlock TextBlock 1
ContentPresenter ContentPresenter → TextBlock 2
Label Label → Border → ContentPresenter → TextBlock 4

Each Label additionally constructs a Border, a ContentPresenter, and the TextBlock that actually draws the characters.
The number of visuals processed during measure and arrange quadruples as well.

Measurements Without Virtualization

Placing the same string in a StackPanel and measuring the time to complete layout, along with the total number of visuals produced, gives the following.

items Label visuals Label ms TextBlock visuals 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

Measured on .NET 10 / Windows 11 with the default theme (Aero2) ControlTemplate, giving Content a string that contains no access key. Elements are placed in a non-virtualized StackPanel. Visual counts change when the theme or ControlTemplate is replaced, and elapsed time depends on the execution environment, so read the values as ratios rather than absolute numbers.

The visual count scales exactly with the element count, and Label is consistently four times that of TextBlock.
The layout time difference, however, stays around a factor of two and does not match the visual ratio.
The number of visuals alone does not determine the time.

Memory was measured too.
After laying out 1,000 elements and forcing a garbage collection, Label still held about 2.6 times the managed heap that TextBlock did.
A Label given a string containing an underscore held about 5.1 times.

  KB vs TextBlock
Label 4,111 2.6x
Label (Content has '_') 8,135 5.1x
TextBlock 1,588 1.0x

Measured in the same environment: 1,000 elements laid out in a StackPanel, taking the difference of GC.GetTotalMemory(true) before and after, median of five runs.

Extra Cost from Underscores in the Content

The cost of Label also varies with the content itself.
When the string contains an underscore (_), the ContentPresenter produces an AccessText rather than a TextBlock, adding one visual layer for a total of five.

Fixing the count at 1,000 and varying the composition gives the following.

  visuals layout ms
Label 4,000 199
Label (Content has '_') 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

Measured in the same environment with 1,000 elements in a StackPanel. A Label whose Content contains an underscore has an AccessText inserted, one more visual than without an underscore, and its layout time was roughly 3.5 times as long. How much of the increase comes from the AccessText itself and how much from the extra visual was not measured. A Label with a TextBlock in its ContentTemplate takes no longer when given an underscore. The AccessText row is for comparison.

The visual count grows only 25%, from four to five, yet layout time rises from 199 ms to 694 ms, roughly a factor of 3.5.
AccessText alone took about 1.8 times as long as TextBlock alone (234 ms against 129 ms).
The increase inside a Label (about 500 ms), however, is larger than the gap between the two alone (about 100 ms), so the cost of AccessText itself does not explain it. The breakdown of the increase was not measured.

A Label with a TextBlock in its ContentTemplate got no AccessText even for a string containing an underscore, and kept four visuals.
Its layout time was 243 ms, no different from a string without an underscore (249 ms).

This matters in practice on screens that display underscore-prone data such as file paths and identifiers.
The separate problem of underscores disappearing from the display is covered in Why WPF Label Hides Underscores and How to Fix It.

With Virtualization Enabled

Everything above compares elements placed directly in a StackPanel, without virtualization.
An ItemsControl with UI virtualization changes the premise.
Only the containers within the visible range and the cache area before and after it, set by VirtualizingPanel.CacheLength, are realized, so the number of simultaneously live visuals stays constant no matter how large the collection grows.

Binding 10,000 items to a virtualized ListBox and swapping only the contents of ItemTemplate gives the following.

  visuals layout ms
Label 238 16
TextBlock 148 13

Measured in the same environment with 10,000 items bound to a ListBox configured with IsVirtualizing="True" and VirtualizationMode="Recycling". Only the containers in the visible range and the cache area before and after it, set by VirtualizingPanel.CacheLength, are realized, so the total visual count does not depend on the item count. The layout time difference is smaller than the difference between repeated runs of the same condition; the roughly 2x gap seen without virtualization does not survive.

Compared with 4,000 elements in a non-virtualized StackPanel, the item count here is 2.5 times larger while layout time is two orders of magnitude smaller.
The gap between Label and TextBlock is smaller than the difference between repeated runs of the same condition.
The medians of 15 runs differ by about 4 ms, while Label alone ranged from 15.5 ms to 114.5 ms.

  min median max
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

The distribution of the 15 runs behind the two tables above, whose values are the minimums.

The primary cause of “many Labels make WPF slow” is not Label itself but the absence of virtualization.
Switching controls buys roughly a factor of two; enabling virtualization buys far more.


Order of Improvements

Work through the improvements in this order.

  1. Confirm that virtualization is in effect first. For list displays, use an ItemsControl-family control and keep UI virtualization enabled. If this is broken, replacing controls yields limited benefit.
  2. Move display-only text to TextBlock. Effective on screens where virtualization is unavailable, or where a fixed but large number of elements is present.
  3. Use ContentTemplate for underscore-bearing data. This keeps Label while avoiding the additional cost of AccessText.

The separation by purpose is as follows.

Step 1: keep virtualization in effect

For list displays, choose a control backed by a virtualizing panel rather than using ItemsControl as-is.
The default ItemsPanel of ItemsControl is a StackPanel, which does not virtualize.

<!-- Not virtualized. Containers are created in proportion to the item count -->
<ItemsControl ItemsSource="{Binding Items}" />

<!-- Virtualized. ListBox defaults to VirtualizingStackPanel -->
<ListBox ItemsSource="{Binding Items}"
         ScrollViewer.CanContentScroll="True"
         VirtualizingPanel.IsVirtualizing="True"
         VirtualizingPanel.VirtualizationMode="Recycling" />

To virtualize while staying on ItemsControl, the ItemsPanel must be set to VirtualizingStackPanel and the template must supply a ScrollViewer.
Even when selection is unnecessary, using ListBox and disabling Focusable and the selection visuals through a style is the more reliable route.

Note that setting ScrollViewer.CanContentScroll to False switches to pixel-based scrolling at the cost of virtualization.
In a ListBox with 2,000 items, 23 ListBoxItems were realized by default, but all 2,000 were realized with CanContentScroll="False".
CanContentScroll defaults to False; the ListBox default style sets it to True.
For smooth scrolling, keep virtualization and set VirtualizingPanel.ScrollUnit="Pixel" instead. The realized ListBoxItems stayed at 23 and only the scroll unit changed to pixels (ExtentHeight went from the item count, 2,000, to 39,920 pixels).

  measured
ScrollViewer.CanContentScroll default False
ListBox (default style) CanContentScroll True, ExtentHeight 2,000, realized 23
ScrollUnit="Pixel" CanContentScroll True, ExtentHeight 39,920, realized 23
CanContentScroll="False" CanContentScroll False, ExtentHeight 39,920, realized 2,000

Measured on .NET 10 / Windows 11 by showing a ListBox with 2,000 items, scrolling to the middle, and counting the ListBoxItems. ExtentHeight is the item count for item-based scrolling and the pixel count for pixel-based scrolling.

Step 2: replace display-only Labels with TextBlock

Replace Label controls used purely for display with TextBlock.
The goal is to reduce rendering cost while preserving appearance.

<!-- Before -->
<Label Content="Status: Running" />

<!-- After -->
<TextBlock Text="Status: Running" />

This removes Label-specific features from display-only locations and unifies them into a lightweight rendering structure.
Because Label’s default Padding is lost, specify Margin or Padding explicitly where needed.

Step 3: use a ContentTemplate for underscore-bearing data

To keep the appearance of Label while avoiding AccessText, specify a TextBlock in ContentTemplate.

<Label Content="{Binding FilePath}">
    <Label.ContentTemplate>
        <DataTemplate>
            <TextBlock Text="{Binding}" />
        </DataTemplate>
    </Label.ContentTemplate>
</Label>

The ContentPresenter no longer produces an AccessText, so underscores render as-is and the rendering cost returns to that of a Label without underscores.
Because ContentTemplate also applies when Content is an object rather than a string, restrict its scope to string display.


Where Label stays

The example below shows an input form caption that requires Label.
This pattern keeps Label to preserve usability and accessibility.

<StackPanel Orientation="Horizontal">
    <Label Content="_Name:"
           Target="{Binding ElementName=NameTextBox}"
           VerticalAlignment="Center" />
    <TextBox x:Name="NameTextBox" Width="180" />
</StackPanel>

Label is appropriate where focus movement via Alt + N and focus movement on label click are required.
Performance work therefore calls for requirement-based separation rather than a blanket ban.

An AccessText is constructed in this example because the access key is intentional, but form captions are few in number and the cost is immaterial.
AccessText cost matters only when displaying large volumes of underscore-bearing data.

Notes and Limitations


Alternatives / Comparison

Approach Advantages Disadvantages Best suited for
Enable virtualization Makes cost independent of item count; the largest single win Scrolling is item-based by default (ScrollUnit="Pixel" makes it pixel-based); incompatible with CanContentScroll="False" Lists and grids with a variable item count
Keep everything as Label Retains access keys and Target exactly as specified High rendering cost at large volumes Form-centric screens with few elements
Replace display-only spots with TextBlock Reduces rendering and memory load Requires revisiting padding and wrapping settings Fixed layouts with many elements that cannot virtualize
Label + ContentTemplate Avoids AccessText cost while preserving appearance More verbose XAML Screens displaying underscore-bearing data in a Label
Convert the whole screen to TextBlock Simple and lightweight Loses Label-specific interaction Read-only screens with no input integration

Summary

The slowdown from placing many WPF Label controls stems from the rendering overhead of its general-purpose ContentControl features.
Each Label constructs four visuals, four times that of TextBlock.
Without virtualization, that translates to roughly 2x in layout time and 2.6x in the managed heap still held after a garbage collection (measured with GC.GetTotalMemory(true), not total memory use).

The practical priority, however, is as follows.

Rather than “avoid Label because it is slow,” first confirm whether virtualization is in effect and whether the screen genuinely renders at volume, then choose according to the requirements.