Performance characteristics of the Silverlight DataGrid

Overview

This article will discuss the performance characteristics of the Silverlight DataGrid both standalone and in comparison with an HTML DataGrid with similar functionality. The project I was working on had a large ASP.NET DataGrid that was performing very badly, and we were asked to rewrite it as a Silverlight component. The HTML DataGrid was delivering in excess of 5MB of HTML and Javascript to the browser when pushed, which was causing excessive load times.

Basic formula

Our initial focus was mainly on load time of the grid, and a simple formula to calculate the load time of the DataGrid was found. Time to load = number of cells created * time taken to create a cell. This seems obvious once you see it, but wasn’t obvious until we’d discovered it. We had a very complex cell template, and clearly if you create a lot of cells, it’s going to take a while to create them.

UI Virtualization

The biggest factor affecting performance was definitely UI virtualization. The idea here is that only the cells that are actually visible on the screen are created, and the others are created on demand as you scroll around the window. This can dramatically affect performance, as if you have a DataGrid with 1000 rows and 20 columns, you’ll create 20,000 cells if you are not careful. However with UI Virtualization your viewport may only be 6 columns wide by 10 rows, meaning only 60 cells are created. This does however, affect scrolling performance as when you scroll, new cells will be created as they scroll into view. If you want to play around with this, just drop a ScrollViewer round your DataGrid. This gives the DataGrid infinite size and effectively turns off virtualization. Unfortunately on my project I’d done this accidentally without realising the effect on performance. You really need to be using the DataGrid’s scroll bars, not a ScrollViewer. Note that if you want to turn off UI Virtualization (eg for small grids) you can re-template the DataGrid and put the RowsPresenter inside a ScrollViewer which again will cause it to think it has infinite size. This is useful because you won’t keep getting LoadingRow and UnloadingRow events as you scroll around. Be careful to do it right so that the column headers scroll correctly (see XAML in the appendix at the end of this article).

Note that the load times of a DataGrid with UI Virtualization are largely independent of the number of rows and columns in the data, and dependant on the number of rows and columns visible in the viewport. You can easily create a DataGrid to view millions of rows.

UI Recycling

Another technique the DataGrid uses for performance is UI Recycling. As rows scroll off the top of the viewport, the cells are re-purposed for new rows scrolling onto the bottom of the viewport by resetting their Data Context to the value of the new row. This behaviour is very visible to the developer as the Loaded event does not fire for the new cells, so any initialisation done in the Loaded event won’t happen. This is okay as long as you use DataBinding for everything and power everything off the DataContext. The Silverlight DataGrid also does horizontal virtualization, but not horizontal recycling as it is likely that the cell template is different for each column. There is no official way to programmatically turn off UI Recycling independently of UI Virtualization, but I found by reading the source of the DataGrid control (it’s on Codeplex) that recycling won’t happen if the templates don’t match, so I used XamlReader.Load to make sure all my templates were different. Again, to reemphasise, you really should be powering everything off the DataContext rather than turning off UI Recycling, but it can come in handy at times.

Load Times

On this project we were replacing an HTML DataGrid with a Silverlight DataGrid and load times were our biggest concern – it was taking 20 seconds to load large HTML grids. We were getting much reduced load times (3 seconds) with the Silverlight DataGrid but still we profiled it with XPerf to really get to the heart of the load times. We found that on an average PC, about 0.9 seconds was being taken before we started executing our code in our App class. Note this was warm start time; the XAP had already been downloaded and cached by the browser. We surmised that this was the time taken to initialise the Silverlight ActiveX control and time to JIT our code, something we’d not really considered. This was Silverlight 3 and the Product Group agreed that these times were reasonable and expected. Note that Silverlight 4 caches the results of JIT’ing so could be potentially faster although it also introduces a new XAML parser which may slow things down again. We’ve not been back and re-analysed startup times with Silverlight 4 yet, but it is expected that times will be similar.

Note that adding columns to the DataGrid but making them invisible is just as expensive as having them visible. We had lots of hidden columns (40%) which were never set visible, so removing them from the DataGrid made a great difference. The data for the columns was still in our data model so could still be accessed but now didn't slow down the UI.

Why should a Silverlight DataGrid be faster than an HTML DataGrid?

At this point it is worth taking a step back and putting what we’ve learned in context – why should a Silverlight DataGrid be faster?

  • With Silverlight the UI code is downloaded only once and then cached. With HTML the markup is downloaded every time. The slower your network, the bigger the difference.
  • The Silverlight grid can be “smarter” on the client and use UI Virtualisation to only render what is actually visible to the user. However, scrolling performance is slower using this technique and an HTML DataGrid once loaded scrolls lighteningly quickly. With UI Virtualization performance is independent of number of rows and you can happily create grids with millions of rows.

A Silverlight grid may be slower in the following instances:

  • There is a fixed cost with instantiating the Silverlight environment. For small grids this may outweigh the other advantages
  • The transformation of the data into UI elements is done on the server in the HTML grid and on the client for the Silverlight grid. Whilst normally this is a good thing as it reduces load on the server and enables better scalability, if the client is a slow PC and the server lightly loaded this may be a bad thing for the end user.

Some things that surprised us

  • HTML rendering engines are very fast and can cope with huge amounts of data. They have been tuned a lot over the years.
  • Large page size does not necessarily give bad performance. During the course of this project the customer optimised their network, introduced compression and upgraded the speed of the network and actually got reasonable performance from 5MB page weights.

Actual performance measurements

We developed a small instrumented test harness to measure performance of the various DataGrids.

The application displayed 70 columns of data and the required number of rows in both the Silverlight DataGrid and HTML DataGrid.

The test harness and browser were both on the local machine so there were no network bandwidth restrictions and consequentially these results do not take into account any payload differences.

clip_image0025_3C65AF3C

The results show that, when not taking the network into consideration, the HTML page renders faster than the Silverlight virtualised grid as long as there less than 20 rows of data. Over 20 rows the time taken to render the HTML grid increases as the amount of data increases whilst the Silverlight virtualised grid’s render time is independent of the amount of data.

Appendix: Non virtualizing DataGrid template

 <UserControl x:Class="NonVirt.MainPage"
    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="https://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="https://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:data="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data"
    xmlns:dataPrimitives="clr-namespace:System.Windows.Controls.Primitives;assembly=System.Windows.Controls.Data"
    mc:Ignorable="d" d:DesignWidth="640" d:DesignHeight="480">

  <UserControl.Resources>
    <Style x:Name="RowScrollViewerStyle" TargetType="ScrollViewer">
      <Setter Property="Template">
        <Setter.Value>
          <ControlTemplate TargetType="ScrollViewer">
            <Border CornerRadius="2" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}">
              <Grid Background="{TemplateBinding Background}">
                <Grid.RowDefinitions>
                  <RowDefinition Height="*"/>
                  <RowDefinition Height="Auto"/>
                </Grid.RowDefinitions>
                <Grid.ColumnDefinitions>
                  <ColumnDefinition Width="*"/>
                  <ColumnDefinition Width="Auto"/>
                </Grid.ColumnDefinitions>
                <ScrollContentPresenter x:Name="ScrollContentPresenter" Cursor="{TemplateBinding Cursor}" Margin="{TemplateBinding Padding}" ContentTemplate="{TemplateBinding ContentTemplate}"/>
                <Rectangle Grid.Column="1" Grid.Row="1" Fill="#FFE9EEF4"/>
                <ScrollBar x:Name="VerticalScrollBar" Width="18" IsTabStop="False" Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility}" Grid.Column="1" Grid.Row="0" Orientation="Vertical" ViewportSize="{TemplateBinding ViewportHeight}" Maximum="{TemplateBinding ScrollableHeight}" Minimum="0" Value="{TemplateBinding VerticalOffset}" Margin="0,-1,-1,-1" />
                <ScrollBar x:Name="HorizontalScrollBar" Grid.Column="0" Grid.Row="1" Height="18" IsTabStop="False" Visibility="Collapsed" Orientation="Horizontal" ViewportSize="{TemplateBinding ViewportWidth}" Maximum="{TemplateBinding ScrollableWidth}" Minimum="0" Value="{TemplateBinding HorizontalOffset}"/>
              </Grid>
            </Border>
          </ControlTemplate>
        </Setter.Value>
      </Setter>
    </Style>

    <Style x:Key="NonVirtualizingDataGridStyle" TargetType="data:DataGrid">
      <Setter Property="Template">
        <Setter.Value>
          <ControlTemplate TargetType="data:DataGrid">
            <Grid>
              <VisualStateManager.VisualStateGroups>
                <VisualStateGroup x:Name="CommonStates">
                  <VisualState x:Name="Normal" />
                  <VisualState x:Name="Disabled">
                    <Storyboard>
                      <DoubleAnimation Storyboard.TargetName="DisabledVisualElement" Storyboard.TargetProperty="Opacity" Duration="0" To="1" />
                    </Storyboard>
                  </VisualState>
                </VisualStateGroup>
              </VisualStateManager.VisualStateGroups>
              <Border BorderThickness="{TemplateBinding BorderThickness}" BorderBrush="{TemplateBinding BorderBrush}" CornerRadius="2">
                <Grid Name="Root" Background="{TemplateBinding Background}">
                  <Grid.Resources>
                    <!--Start: TopLeftHeaderTemplate-->
                    <ControlTemplate x:Key="TopLeftHeaderTemplate" TargetType="dataPrimitives:DataGridColumnHeader">
                      <Grid Name="Root">
                        <Grid.RowDefinitions>
                          <RowDefinition />
                          <RowDefinition />
                          <RowDefinition Height="Auto" />
                        </Grid.RowDefinitions>
                        <Border BorderThickness="0,0,1,0" BorderBrush="#FFC9CACA" Background="#FF1F3B53" Grid.RowSpan="2">
                          <Rectangle Stretch="Fill" StrokeThickness="1">
                            <Rectangle.Fill>
                              <LinearGradientBrush StartPoint=".7,0" EndPoint=".7,1">
                                <GradientStop Color="#FCFFFFFF" Offset="0.015" />
                                <GradientStop Color="#F7FFFFFF" Offset="0.375" />
                                <GradientStop Color="#E5FFFFFF" Offset="0.6" />
                                <GradientStop Color="#D1FFFFFF" Offset="1" />
                              </LinearGradientBrush>
                            </Rectangle.Fill>
                          </Rectangle>
                        </Border>
                        <Rectangle VerticalAlignment="Bottom" Width="Auto" StrokeThickness="1" Height="1" Fill="#FFDBDCDC" Grid.RowSpan="2" />
                      </Grid>
                    </ControlTemplate>
                    <!--End: TopLeftHeaderTemplate-->

                    <!--Start: TopRightHeaderTemplate-->
                    <ControlTemplate x:Key="TopRightHeaderTemplate" TargetType="dataPrimitives:DataGridColumnHeader">
                      <Grid Name="RootElement">
                        <Grid.RowDefinitions>
                          <RowDefinition />
                          <RowDefinition />
                          <RowDefinition Height="Auto" />
                        </Grid.RowDefinitions>
                        <Border BorderThickness="1,0,0,0" BorderBrush="#FFC9CACA" Background="#FF1F3B53" Grid.RowSpan="2">
                          <Rectangle Stretch="Fill">
                            <Rectangle.Fill>
                              <LinearGradientBrush StartPoint=".7,0" EndPoint=".7,1">
                                <GradientStop Color="#FCFFFFFF" Offset="0.015" />
                                <GradientStop Color="#F7FFFFFF" Offset="0.375" />
                                <GradientStop Color="#E5FFFFFF" Offset="0.6" />
                                <GradientStop Color="#D1FFFFFF" Offset="1" />
                              </LinearGradientBrush>
                            </Rectangle.Fill>
                          </Rectangle>
                        </Border>
                      </Grid>
                    </ControlTemplate>
                    <!--End: TopRightHeaderTemplate-->
                  </Grid.Resources>

                  <Grid.RowDefinitions>
                    <RowDefinition Height="Auto" />
                    <RowDefinition />
                    <RowDefinition Height="Auto" />
                    <RowDefinition Height="Auto" />
                  </Grid.RowDefinitions>
                  <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="Auto" />
                    <ColumnDefinition />
                    <ColumnDefinition Width="Auto" />
                  </Grid.ColumnDefinitions>

                  <!-- Fill up the space in the header above the vertical scroll bar -->
                  <Grid Grid.Column="1">
                    <Grid.RowDefinitions>
                      <RowDefinition Height="0.5*" />
                      <RowDefinition Height="0.5*" />
                    </Grid.RowDefinitions>
                    <Rectangle x:Name="BackgroundGradient" Stretch="Fill" Fill="#FF79C1C0" Grid.RowSpan="2"/>
                    <Rectangle x:Name="BackgroundGradient_Copy" Fill="#6BFFFFFF" VerticalAlignment="Stretch" Grid.RowSpan="1"/>
                  </Grid>

                  <dataPrimitives:DataGridColumnHeader Name="TopLeftCornerHeader" Template="{StaticResource TopLeftHeaderTemplate}" Width="22" />
                  <dataPrimitives:DataGridColumnHeadersPresenter Grid.Column="1" Name="ColumnHeadersPresenter" HorizontalAlignment="Left"/>
                  <dataPrimitives:DataGridColumnHeader Name="TopRightCornerHeader" Grid.Column="2" Template="{StaticResource TopRightHeaderTemplate}" />
                  <Rectangle Name="ColumnHeadersAndRowsSeparator" Grid.ColumnSpan="3" VerticalAlignment="Bottom" Width="Auto" StrokeThickness="1" Height="1" Fill="#FFC9CACA"/>

                  <ScrollViewer Style="{StaticResource RowScrollViewerStyle}" Grid.ColumnSpan="2" Grid.Row="1" Padding="0" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled" >
                    <dataPrimitives:DataGridRowsPresenter Name="RowsPresenter" />
                  </ScrollViewer>
                  <Rectangle Name="BottomRightCorner" Fill="#FFE9EEF4" Grid.Column="2" Grid.Row="2" />
                  <Rectangle Name="BottomLeftCorner" Fill="#FFE9EEF4" Grid.Row="2" Grid.ColumnSpan="2" />
                  <Grid Grid.Column="1" Grid.Row="2">
                    <Grid.ColumnDefinitions>
                      <ColumnDefinition Width="Auto" />
                      <ColumnDefinition />
                    </Grid.ColumnDefinitions>
                    <Rectangle Name="FrozenColumnScrollBarSpacer" />
                    <!--<local:Navigator Margin="4,0,2,0" />-->
                    <ScrollBar Name="HorizontalScrollbar" Grid.Column="1" Orientation="Horizontal" Height="18" Margin="-1,0,-1,-1" />
                  </Grid>
                </Grid>
              </Border>
              <Border x:Name="DisabledVisualElement" IsHitTestVisible="False" Height="Auto" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Width="Auto" CornerRadius="2" Background="#8CFFFFFF" Opacity="0"/>
            </Grid>
          </ControlTemplate>
        </Setter.Value>
      </Setter>
    </Style>

  </UserControl.Resources>

  <Grid>
    <data:DataGrid x:Name="datagrid" Style="{StaticResource NonVirtualizingDataGridStyle}" FrozenColumnCount="2" AutoGenerateColumns="True"/>
  </Grid>
</UserControl>

Written by Paul Tallett