Performance Optimization
Techniques for maximizing rendering speed, reducing layout overhead, and scaling Beep applications to large datasets
Table of Contents
Overview
Beep Controls are built on top of GDI+ with double-buffering enabled by default. However, as your application scales — more rows in a grid, more controls on a form, multiple monitors at different DPI settings — you need to employ the right optimization techniques to keep the UI responsive.
This guide covers the key levers available in the Beep framework: DPI-aware painting, smart invalidation, update batching, virtual data modes, and debounced rendering.
DPI-Aware Rendering
Beep controls automatically scale their rendering based on the current monitor's DPI setting.
BaseControl exposes a ScaleFactor property that reflects the
current DPI ratio (e.g., 1.0 at 96 dpi, 1.5 at 144 dpi, 2.0 at 192 dpi). Use this factor
in custom paint logic instead of hard-coded pixel values.
protected override void OnPaint(PaintEventArgs e)
{
float sf = ScaleFactor;
// Scale dimensions with DPI
int iconSize = (int)(24 * sf);
int padding = (int)(8 * sf);
int cornerRadius = (int)(6 * sf);
// Use scaled values for drawing
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
using var path = RoundedRectangle(ClientRectangle, cornerRadius);
e.Graphics.FillPath(_backBrush, path);
}
When a form moves between monitors with different DPI settings, BaseControl
re-reads the device context and updates ScaleFactor. Override
OnDpiChanged to react:
protected override void OnDpiChanged(DpiChangedEventArgs e)
{
base.OnDpiChanged(e);
// ScaleFactor is already updated by base
RebuildImageCache();
Invalidate();
}
EnableSmartInvalidation
By default, calling Invalidate() marks the entire control rectangle for repaint.
With EnableSmartInvalidation = true, BaseControl tracks dirty regions
so that only parts of the control that actually changed are repainted. This is especially
beneficial for controls that update frequently (e.g., status bars, live data displays).
public class LiveStatusBar : BaseControl
{
public LiveStatusBar()
{
EnableSmartInvalidation = true;
}
private string _statusText = "Ready";
public string StatusText
{
get => _statusText;
set
{
_statusText = value;
// Only invalidates the text area, not the entire bar
Rectangle dirty = GetTextBounds();
Invalidate(dirty);
}
}
}
BeginUpdate / EndUpdate Batching
When you need to change multiple properties on a control at once, wrap the changes in
BeginUpdate() / EndUpdate(). This suppresses intermediate
layout calculations and repaints, performing a single update at the end.
// Without batching: each line triggers Invalidate() and layout
myGrid.Columns.Clear();
myGrid.Columns.Add(new GridColumn("Id"));
myGrid.Columns.Add(new GridColumn("Name"));
myGrid.Columns.Add(new GridColumn("Email"));
myGrid.DataSource = source;
// 4+ repaints
// With batching: single repaint
myGrid.BeginUpdate();
myGrid.Columns.Clear();
myGrid.Columns.Add(new GridColumn("Id"));
myGrid.Columns.Add(new GridColumn("Name"));
myGrid.Columns.Add(new GridColumn("Email"));
myGrid.Columns.Add(new GridColumn("Phone"));
myGrid.Columns.Add(new GridColumn("Status"));
myGrid.DataSource = source;
myGrid.EndUpdate(); // Single repaint and layout pass
Nesting is supported — EndUpdate() only fires when the outermost call completes:
myGrid.BeginUpdate();
myGrid.Columns.Add(new GridColumn("A"));
myGrid.BeginUpdate(); // Counter = 2
myGrid.Columns.Add(new GridColumn("B"));
myGrid.Columns.Add(new GridColumn("C"));
myGrid.EndUpdate(); // Counter = 1, no repaint yet
myGrid.Columns.Add(new GridColumn("D"));
myGrid.EndUpdate(); // Counter = 0, single repaint now
SuspendLayout / ResumeLayout
When adding or removing child controls from a container, use the WinForms
SuspendLayout()/ResumeLayout() pair. This prevents the layout
engine from recalculating positions after each individual addition.
panel.SuspendLayout();
for (int i = 0; i < 200; i++)
{
var label = new BeepLabel
{
Text = $"Row {i}",
Location = new Point(10, i * 28),
Size = new Size(300, 26)
};
panel.Controls.Add(label);
}
panel.ResumeLayout(true);
Combine SuspendLayout() with BeginUpdate() for maximum benefit
when both adding controls and changing properties:
form.SuspendLayout();
grid.BeginUpdate();
// Bulk operations here...
grid.EndUpdate();
form.ResumeLayout(true);
Virtual Mode for BeepGridPro
When displaying datasets with thousands of rows, set VirtualMode = true.
Instead of creating row objects for every data item, the grid requests cell values
on-demand via the CellValueNeeded event, dramatically reducing memory and
startup time.
public class LargeDataForm : BeepiFormPro
{
private List _allCustomers;
public LargeDataForm()
{
_allCustomers = LoadAllCustomers(); // 100,000+ rows
var grid = new BeepGridPro();
grid.VirtualMode = true;
grid.RowCount = _allCustomers.Count;
grid.Columns.Add(new GridColumn("Id"));
grid.Columns.Add(new GridColumn("Name"));
grid.Columns.Add(new GridColumn("Email"));
grid.CellValueNeeded += (s, e) =>
{
var customer = _allCustomers[e.RowIndex];
e.Value = e.ColumnIndex switch
{
0 => customer.Id,
1 => customer.Name,
2 => customer.Email,
_ => null
};
};
Controls.Add(grid);
}
}
Debounced Painting
When responding to rapid-fire events (mouse move, resize, scroll), painting on every
event can cause stutter. Use debouncing to coalesce multiple paint requests into one.
The DebounceInvalidate helper skips repaints that are queued within a
configurable time window.
public class SmoothChart : BaseControl
{
private readonly Timer _debounceTimer = new Timer { Interval = 16 };
public SmoothChart()
{
_debounceTimer.Tick += (s, e) =>
{
_debounceTimer.Stop();
Invalidate();
};
}
protected override void OnResize(EventArgs e)
{
base.OnResize(e);
DebounceInvalidate(); // Fires at most once per ~16ms (60 fps)
}
private void DebounceInvalidate()
{
_debounceTimer.Stop();
_debounceTimer.Start();
}
}
Caching GDI+ Resources
Creating Brush, Pen, and Font objects in
OnPaint is a common performance pitfall. Cache them as fields and only
recreate when the theme changes.
public class BadgeControl : BaseControl
{
private SolidBrush _bgBrush;
private SolidBrush _textBrush;
private Pen _borderPen;
protected override void ApplyControlStyle()
{
base.ApplyControlStyle();
var theme = BeepThemesManager.ActiveTheme;
_bgBrush?.Dispose();
_textBrush?.Dispose();
_borderPen?.Dispose();
_bgBrush = new SolidBrush(theme.AccentColor);
_textBrush = new SolidBrush(theme.ForeColor);
_borderPen = new Pen(theme.BorderColor, 1f);
}
protected override void OnPaint(PaintEventArgs e)
{
e.Graphics.FillRectangle(_bgBrush, ClientRectangle);
e.Graphics.DrawRectangle(_borderPen, 0, 0, Width - 1, Height - 1);
// Draw text using _textBrush
}
}
Layout and Control Count Tips
- Use BeepSimpleGrid for tabular layouts — instead of creating 50+ individual labels and textboxes.
- Use BeepTree for tree structures — avoid nesting dozens of expandable panels.
- Use BeepListBox for selectable lists — don't create individual checkboxes for each item.
- Limit form control count — forms with 300+ controls exhibit measurable GDI+ overhead. Consider tab-based layouts.
- Lazy-load tabs — populate tab pages only when the user navigates to them, not at form startup.
- Use Pagination — combine
BeepDataNavigatorwith server-side paging instead of loading all rows into the grid. - Profile with a debug build — measure paint times with
Stopwatchto identify bottlenecks.