Performance Tuning
Performance tuning guide for RichTextEditor. Covers batch edits, event optimization, memory management, serialization, and profiling strategies.
info
This control is available as part of Avalonia Pro or higher.
Core performance characteristics
Time complexity
| Operation | Complexity | Notes |
|---|---|---|
| Insert text | O(log n) | Rope data structure |
| Delete text | O(log n) | Balanced tree update |
| Find position | O(log n) | Tree traversal |
| Undo/Redo | O(1) - O(log n) | Structural undo |
| Serialize | O(n) | Streaming tokenizer |
| Render | O(visible nodes) | Viewport culling |
Memory usage
- Document: O(n) text + O(m) nodes
- Rope overhead: ~2x base text
- Undo stack: ~10% with structural undo (vs 100x traditional)
- Snapshots: Shared structure, minimal overhead
Performance checklist
- Batch all multi-edit operations
- Use
UpdateFinishedinstead ofChangedfor expensive operations - Debounce user-triggered updates
- Set appropriate
UndoLimiton the editor - Disable undo during bulk loads
- Use background threads for serialization
- Minimize pointer allocations
- Profile before optimizing
Batch edit optimization
Always batch multiple operations
Single change notification instead of one per edit:
// Bad — 100 Changed events, 100 layout passes
for (int i = 0; i < 100; i++)
{
pointer.InsertText("Line " + i + "\n");
}
// Good — 1 UpdateFinished event, 1 layout pass
using (document.BeginChange())
{
for (int i = 0; i < 100; i++)
{
pointer.InsertText("Line " + i + "\n");
}
}
Impact: 10-100x speedup for bulk operations.
Event handler optimization
Defer expensive operations
Use UpdateFinished instead of reacting to every edit:
// Bad — called for every keystroke
editor.ContentChanged += (s, e) =>
{
RebuildUI();
};
// Good — called once per batch
var textDoc = editor.Document?.TextDocument;
textDoc.UpdateFinished += (s, e) =>
{
RebuildUI();
};
Debounce user-triggered updates
private DispatcherTimer _updateTimer;
void Setup()
{
_updateTimer = new DispatcherTimer
{
Interval = TimeSpan.FromMilliseconds(300)
};
_updateTimer.Tick += OnDelayedUpdate;
editor.ContentChanged += (s, e) =>
{
_updateTimer.Stop();
_updateTimer.Start(); // Restart timer
};
}
void OnDelayedUpdate(object? sender, EventArgs e)
{
_updateTimer.Stop();
// Expensive operation (word count, spell check, etc.)
UpdateStatistics();
}
Impact: Reduces CPU usage during continuous typing.
Pointer and range optimization
Minimize pointer allocations
// Bad — creating a pointer per character index
for (int i = 0; i < 1000; i++)
{
var p = document.ContentStart.GetPositionAtOffset(i); // O(log n) per call
}
// Good — snapshot once and read text in bulk
var snapshot = document.CreateSnapshot();
string slice = snapshot.GetText(offset: 0, length: 1000);
for (int i = 0; i < slice.Length; i++)
{
char c = slice[i]; // direct array access
}
Reuse pointers when possible
var pointer = document.ContentStart;
for (int i = 0; i < 100; i++)
{
pointer.InsertText("Line\n");
// pointer auto-updates to after insertion
}
Memory management
Undo stack limits
// Default: 100 operations (set via RichTextEditor.UndoLimit)
editor.UndoLimit = 50; // Reduce for memory-constrained environments
editor.UndoLimit = 200; // Increase for power users
Trade-off: Memory vs undo history depth.
Disable undo for bulk loads
void LoadLargeDocument(string rtfPath)
{
var undoManager = editor.UndoManager;
if (undoManager != null)
undoManager.IsEnabled = false;
try
{
using var stream = File.OpenRead(rtfPath);
editor.Load(stream, new RtfSerializer());
}
finally
{
if (undoManager != null)
undoManager.IsEnabled = true;
}
}
Impact: 50% faster load, no undo memory overhead.
Clear undo history when needed
// After saving document
editor.ClearUndoHistory();
Serialization performance
Use background threads
async Task SaveDocumentAsync(string path)
{
// SaveAsync handles snapshot creation internally
await using var stream = File.Create(path);
await editor.SaveAsync(stream, new RtfSerializer());
}
Impact: No UI blocking during save.
Stream large files
// Streaming tokenizer handles large files efficiently
await using var stream = File.OpenRead("large.rtf");
await editor.LoadAsync(stream, new RtfSerializer());
// Memory usage: O(output size), not O(file size)
Rendering performance
Viewport culling
Built-in: only visible elements are rendered. No action needed.
Reduce layout passes
// Batch formatting changes
using (document.BeginChange())
{
range1.ApplyPropertyValue(prop1, value1);
range2.ApplyPropertyValue(prop2, value2);
range3.ApplyPropertyValue(prop3, value3);
}
// Single layout pass
Simplify complex documents
- Limit nesting depth (< 10 levels)
- Merge adjacent runs with same formatting
- Use metadata normalization
Large document strategies
Tested limits
- 10,000+ paragraphs
- 1MB+ RTF files
- 100+ undo operations
For very large documents (100MB+)
Consider:
- Pagination — load sections on demand
- Virtual scrolling — render visible pages only
- Read-only mode — disable undo for memory savings via
FlowDocumentScrollViewer - Streaming — process in chunks
Benchmarking
Built-in benchmarks
cd benchmarks/Avalonia.Controls.Documents.Benchmarks
dotnet run -c Release
Benchmarks cover:
- Text insertion/deletion
- Batch edits
- Serialization
- Metadata normalization
Custom benchmarks
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
[MemoryDiagnoser]
public class CustomBenchmark
{
private TextDocument _document;
[GlobalSetup]
public void Setup()
{
_document = new TextDocument("Initial text");
}
[Benchmark]
public void BulkInsert()
{
var pointer = _document.ContentStart;
_document.BeginChange();
for (int i = 0; i < 1000; i++)
{
pointer.InsertText("X");
}
_document.EndChange();
}
}
BenchmarkRunner.Run<CustomBenchmark>();
Anti-patterns
Don't poll document state
// Bad: Polling
var timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(100) };
timer.Tick += (s, e) => CheckDocumentState();
// Good: Event-driven
var textDoc = editor.Document?.TextDocument;
if (textDoc != null)
textDoc.UpdateFinished += (s, e) => UpdateState();
Don't rebuild UI on every keystroke
// Bad
editor.ContentChanged += (s, e) => RebuildEntireUI();
// Good
var textDoc = editor.Document?.TextDocument;
if (textDoc != null)
{
textDoc.UpdateFinished += (s, e) =>
{
if (e.HasChanges)
RefreshAffectedRegions();
};
}
Don't store full text copies for undo
// Bad: Undo via full text
undoStack.Push(editor.Document?.ContentRange?.Text ?? "");
// Good: Built-in UndoManager (auto-created by the editor)
editor.UndoLimit = 100;
Profiling tips
Use diagnostic tools
Windows: Visual Studio Performance Profiler
macOS/Linux: dotnet-trace, PerfView
Hot paths to monitor
TextDocument.InsertText/DeleteTextRopeTextStoreoperations- Layout in
FlowDocumentView - Event handlers (
Changed,UpdateFinished)
Red flags
- O(n^2) algorithms in custom event handlers
- Excessive allocations (>1MB for simple edits)
- Layout thrashing (multiple passes per edit)
- Unbounded undo growth