| DS01 |
Dictionary<BindableProperty, BindablePropertyContext> |
BindableObject._properties eager init (cap 4) |
Evaluate lower initial capacity / compact storage strategy |
Lower baseline dictionary + bucket footprint |
High |
| DS02 |
Dictionary<TriggerBase, SetterSpecificity> |
BindableObject._triggerSpecificity eager init |
Lazy-init on first trigger usage |
Avoid dictionary alloc for trigger-free objects |
Low |
| DS03 |
Dictionary<Size, SizeRequest> |
VisualElement._measureCache eager init |
Lazy-init on first measure cache write |
Avoid cache alloc for simple/one-shot elements |
Medium |
| DS04 |
Dictionary<BindableProperty,(string,SetterSpecificity)> |
Element._dynamicResources lazy field but often forced early |
Ensure true on-demand path everywhere |
Reduce early dictionary creation |
Low |
| DS05 |
Dictionary<BindableProperty,(string,SetterSpecificity)> |
MergedStyle._defaultStyleProperties used by style plumbing |
Delay creation until first implicit/class style mutation |
Reduce style dictionary overhead in unstyled cases |
Medium |
| DS06 |
HashSet<string> |
Element._pendingHandlerUpdatesFromBPSet |
Strict lazy-init path only when handler update batching starts |
Avoid set alloc for elements without pending updates |
Low |
| DS07 |
Int32[] bucket arrays |
Backing for dictionaries/hashsets |
Reduce initial capacities where safe |
Smaller initial bucket arrays |
Medium |
| DS08 |
Entry<TKey,TValue>[] arrays |
Dictionary entries arrays |
Delay growth; tune first-resize thresholds |
Fewer entry-array allocations |
Medium |
| DS09 |
SetterSpecificityList<object> |
BindablePropertyContext.Values |
Lazy-init when non-default value path is used |
Avoid list alloc on untouched/default properties |
Low |
| DS10 |
SetterSpecificityList<BindingBase> |
BindablePropertyContext.Bindings |
Lazy-init only when bindings are attached |
Avoid bindings list alloc for non-bound properties |
Low |
| DS11 |
Queue<SetValueArgs>-style delayed setters |
BindablePropertyContext delayed work |
Lazy-init only during batched set scenarios |
Avoid queue alloc for non-batched paths |
Low |
| DS12 |
Object[] backing arrays |
SetterSpecificityList<object>.Values |
Tighten initial capacity / delayed allocate backing |
Fewer backing-array allocations |
Low |
| DS13 |
BindableProperty[] backing arrays |
SetterSpecificityList keys |
Delay keys array creation until second insertion |
Avoid arrays for singleton/default cases |
Low |
| DS14 |
SetterSpecificity[] backing arrays |
SetterSpecificityList specificities |
Delay/init with smaller footprint |
Reduce small-array overhead |
Low |
| DS15 |
List<BindableProperty> |
MergedStyle._implicitStyles |
Delay list creation until first implicit style registration |
Avoid list alloc in style-light trees |
Medium |
| DS16 |
IList<BindableProperty> / IList<Style> |
MergedStyle class-style tracking |
Allocate only when StyleClass is actually set |
Avoid class-style list allocations |
Low |
| DS17 |
IList<Element> / List<Element> |
Element._internalChildren |
Keep null or shared empty until first child add |
Save child-list alloc for leaf elements |
Low |
| DS18 |
List<Action<object,ResourcesChangedEventArgs>> |
Element._changeHandlers |
Single-subscriber fast path + lazy list promotion |
Avoid list alloc in 0/1-handler cases |
Low |
| DS19 |
IList<BindableObject> |
Element._bindableResources |
Strict lazy-init and avoid pre-creation |
Save list alloc when no bindable resources |
Low |
| DS20 |
TrackableCollection<Effect> |
Element._effects |
Lazy-create only on first effects access |
Save collection alloc for effect-free elements |
Low |
| DS21 |
ObservableCollection<IGestureRecognizer> |
View._gestureRecognizers eager init |
Lazy-init on first gesture registration |
Remove always-paid collection cost on gesture-free views |
Medium |
| DS22 |
List<IGestureRecognizer> |
Backing list inside ObservableCollection |
Delay internal list creation via lazy outer collection |
Avoid nested list alloc when unused |
Medium |
| DS23 |
ObservableCollection<IGestureRecognizer> |
View._compositeGestureRecognizers |
Maintain strict on-demand path |
Prevent accidental eager creation |
Low |
| DS24 |
Lazy<List<Page>> |
NavigationProxy push stack wrapper |
Replace wrapper with nullable list + manual lazy init |
Remove Lazy + LazyHelper overhead |
Medium |
| DS25 |
Lazy<NavigatingStepRequestList> |
NavigationProxy modal stack wrapper |
Replace wrapper with nullable field + manual lazy init |
Remove Lazy + LazyHelper overhead |
Medium |
| DS26 |
Lazy<T> wrappers (general) |
Base-path internals where thread-safe lazy is unnecessary |
Convert to nullable field + ??= in single-threaded UI paths |
Remove wrapper/helper/delegate objects |
Medium |
| DS27 |
LazyHelper internal objects |
Created by Lazy<T> |
Eliminated indirectly by DS24–DS26 |
Fewer helper allocations per instance |
Medium |
| DS28 |
Func<T> factory delegates |
Factory delegates captured by Lazy<T> |
Replace with direct initializer methods and null checks |
Remove delegate allocations |
Low |
| DS29 |
Delegate objects (EventHandler*) |
Constructor-time subscriptions and event fields |
Defer subscriptions where safe; static delegate caching |
Fewer per-instance delegate allocations |
Medium |
| DS30 |
WeakReference / WeakReference<Element> |
_inheritedContext, _parentOverride, _realParent |
Audit for nullable direct refs + explicit lifecycle points |
Reduce weak-ref object churn |
Medium |
| DS31 |
Weak proxy wrappers |
Background/clip/shadow proxy wrappers |
Ensure strict on-demand creation and reuse patterns |
Avoid proxy alloc on feature-unused elements |
Low |
| DS32 |
Boolean flag fields |
Multiple bools in VisualElement/Element/BindableObject |
Bit-pack to a compact flags field |
Reduce object size + padding waste |
Medium |
| DS33 |
Small scalar state (ushort, nullable ids) |
_triggerCount, _id and related state |
Pack/co-locate infrequent state with flags or deferred generation |
Minor but broad per-instance savings |
Low |
| DS34 |
Struct-heavy state slots |
_frame, mock bounds and similar fields |
Verify necessity of always-live slots vs deferred state blocks |
Potential object-size reduction |
High |
PoC PR: #34150
Summary
Reduce per-instance memory usage and startup allocations by optimizing the shared underlying data structures in the control base chain:
View→VisualElement→NavigableElement→StyleableElement→Element→BindableObjectThis issue focuses on structure-level costs (dictionaries, lists, sets, arrays, weak refs, lazy wrappers, delegate objects), not on control-specific feature work.
Scope
In scope:
Out of scope:
Goals
new Label()to app-like startup emulationGuardrails
_properties, layout internals) require dedicated micro + integration benchmarks before code changesData-structure opportunity list
The table below catalogs all identified optimization candidates in the base chain. Each candidate (DS01–DS34) represents one concrete storage/allocation site.
DS01–DS34 opportunity table (click to expand)
Dictionary<BindableProperty, BindablePropertyContext>BindableObject._propertieseager init (cap 4)Dictionary<TriggerBase, SetterSpecificity>BindableObject._triggerSpecificityeager initDictionary<Size, SizeRequest>VisualElement._measureCacheeager initDictionary<BindableProperty,(string,SetterSpecificity)>Element._dynamicResourceslazy field but often forced earlyDictionary<BindableProperty,(string,SetterSpecificity)>MergedStyle._defaultStylePropertiesused by style plumbingHashSet<string>Element._pendingHandlerUpdatesFromBPSetInt32[]bucket arraysEntry<TKey,TValue>[]arraysSetterSpecificityList<object>SetterSpecificityList<BindingBase>Queue<SetValueArgs>-style delayed settersObject[]backing arraysSetterSpecificityList<object>.ValuesBindableProperty[]backing arraysSetterSpecificityListkeysSetterSpecificity[]backing arraysSetterSpecificityListspecificitiesList<BindableProperty>MergedStyle._implicitStylesIList<BindableProperty>/IList<Style>MergedStyleclass-style trackingStyleClassis actually setIList<Element>/List<Element>Element._internalChildrenList<Action<object,ResourcesChangedEventArgs>>Element._changeHandlersIList<BindableObject>Element._bindableResourcesTrackableCollection<Effect>Element._effectsObservableCollection<IGestureRecognizer>View._gestureRecognizerseager initList<IGestureRecognizer>ObservableCollectionObservableCollection<IGestureRecognizer>View._compositeGestureRecognizersLazy<List<Page>>NavigationProxypush stack wrapperLazy+LazyHelperoverheadLazy<NavigatingStepRequestList>NavigationProxymodal stack wrapperLazy+LazyHelperoverheadLazy<T>wrappers (general)??=in single-threaded UI pathsLazyHelperinternal objectsLazy<T>Func<T>factory delegatesLazy<T>EventHandler*)WeakReference/WeakReference<Element>_inheritedContext,_parentOverride,_realParentVisualElement/Element/BindableObjectushort, nullable ids)_triggerCount,_idand related state_frame, mock bounds and similar fieldsRecommended evaluation order
_triggerSpecificitydictionary_dynamicResourcesdictionary creation_internalChildrennull/shared-empty until first child addTrackableCollection<Effect>_changeHandlersBindablePropertyContext.BindingslistLazy<T>wrappers with nullable fields where safe_measureCachewith perf guardrailsBenchmarking plan
Benchmark ladder
new object()/ empty baselinenew Label()baselinenew Label()+ minimal property set (Text,IsVisible)new Label()+ feature toggles (gestures, effects, dynamic resources, style class, navigation)Required benchmark dimensions
Allocatedbytes/op, Gen0/Gen1 countsgcdumpspot checksSuccess criteria
Allocation breakdown:
new Label()— 3,112 bytesField summary
Object header (MethodTable ptr + sync block) adds 16 bytes. With alignment padding the Label object occupies 664 bytes on the GC heap.
The remaining ~2,448 bytes come from 29 subsidiary heap objects allocated eagerly in constructors and field initializers.
Per-field allocation detail (click to expand)
BindableObject — 9 fields (59 B raw)
_dispatcherIDispatcher_triggerCountushort_triggerSpecificityDictionary<TriggerBase, SetterSpecificity>_propertiesDictionary<BindableProperty, BindablePropertyContext>_applyingbool_inheritedContextWeakReferencePropertyChanged(event)PropertyChangedEventHandlerPropertyChanging(event)PropertyChangingEventHandlerBindingContextChanged(event)EventHandlerStyleableElement — 1 field (8 B raw)
_mergedStyleMergedStyleElement — 26 fields (224 B raw)
_bindableResourcesIList<BindableObject>_changeHandlersList<Action<object, ResourcesChangedEventArgs>>_dynamicResourcesDictionary<BindableProperty, (string, SetterSpecificity)>_effectControlProviderIEffectControlProvider_effectsTrackableCollection<Effect>_idGuid?_parentOverrideWeakReference<Element>_styleIdstring_logicalChildrenReadonlyIReadOnlyList<Element>_internalChildrenIList<Element>_realParentWeakReference<Element>transientNamescopeINameScope_pendingHandlerUpdatesFromBPSetHashSet<string>_currentPropertyBeingSetBindableProperty_handlerIElementHandler_effectsFactoryEffectsFactory_previousHandlerIElementHandlerEventHandler<T>NavigableElement — 0 fields
No instance fields. Constructor sets
Navigation = new NavigationProxy()which goes throughSetValueon aBindableProperty(stored in_propertiesdictionary, not as a field).VisualElement — 38 fields (260 B raw)
_inputTransparentExplicit,_isEnabledExplicitbool_effectiveVisualIVisual_measureCacheDictionary<Size, SizeRequest>_batchedint_computedConstraintLayoutConstraintbool_mockHeight/Width/X/Ydouble_selfConstraintLayoutConstraint_resourcesResourceDictionarybool_frameRect(4 × double)_semanticsSemantics?View — 5 fields (40 B raw)
_gestureRecognizersObservableCollection<IGestureRecognizer>_recognizerForPointerOverStatePointerGestureRecognizer_compositeGestureRecognizersObservableCollection<IGestureRecognizer>_gestureManagerGestureManagerpropertyMapperPropertyMapperLabel — 1 field (8 B raw)
_platformConfigurationRegistryLazy<PlatformConfigurationRegistry<Label>>Subsidiary heap allocations — 29 objects, ~2,448 B (click to expand)
These are objects allocated eagerly in constructors or field initializers. Each one is a separate GC heap object that contributes to the 3,112 B total.
Measured via
dotnet-gcdump— object counts at exactly 10,000 (1× per Label) or 30,000 (3× per Label).Int32[]_properties+_dynamicResources+_defaultStylePropertiesEntry<BindableProperty, BindablePropertyContext>[]_propertiesdictionary internal entries arrayEventHandlerPropertyChanged+PropertyChanging+BindingContextChangedsubscriber delegatesEntry<BindableProperty, (string, SetterSpecificity)>[]MergedStyle._defaultStylePropertiesinternal entries arrayObject[]SetterSpecificityList<object>.Valuesbacking arrayLazyHelperLazy<T>fieldsDictionary<BindableProperty, BindablePropertyContext>BindableObject._propertiesheaderBindableProperty[]SetterSpecificityListkeys backing arrayDictionary<BindableProperty, (string, SetterSpecificity)>MergedStyle._defaultStylePropertiesheaderMergedStyleStyleableElement._mergedStyleDictionary<Size, SizeRequest>VisualElement._measureCacheEntry<string>[]_pendingHandlerUpdatesFromBPSet(HashSet) internal entries arrayHashSet<string>Element._pendingHandlerUpdatesFromBPSetheaderGestureManagerView._gestureManagerNotifyCollectionChangedEventHandlerObservableCollection.CollectionChangedsubscriber delegateFunc<PlatformConfigurationRegistry<Label>>Lazy<PlatformConfigurationRegistry<Label>>EventHandler<HandlerChangingEventArgs>MergedStylesubscribes toElement.HandlerChangingBindingPropertyChangedDelegateNavigationProperty.PropertyChangedcallbackBindablePropertyContextNavigationPropertydefault value set in ctorObservableCollection<IGestureRecognizer>View._gestureRecognizersSetterSpecificityList<object>BindablePropertyContext.ValuesSetterSpecificityList<BindingBase>BindablePropertyContext.BindingsNavigationProxyNavigableElement()ctorLazy<List<Page>>Lazy<PlatformConfigurationRegistry<Label>>Label._platformConfigurationRegistryLazy<NavigatingStepRequestList>List<IGestureRecognizer>ObservableCollectioninternal backing listList<BindableProperty>SetterSpecificity[]SetterSpecificityListspecificities backing arrayRemaining ~160 bytes (3,112 − 2,952) are from small transient objects not captured in the heap snapshot.
Allocation by category
Key observations
Dictionaries/HashSet allocated eagerly (712 B, 23%) —
_propertiesdictionary is needed immediately but initial capacity may be too large for controls that use few properties;_measureCacheand_pendingHandlerUpdatesFromBPSetcould be lazy.MergedStyle + backing dictionary (280 B, 9%) — allocated in
StyleableElementctor for every element, even those that never use styles.Gesture infrastructure (152 B, 5%) —
ObservableCollection+List+GestureManager+ delegate allocated inViewfield initializer even for controls that never use gestures.3× Lazy<T> fields (216 B, 7%) — each
Lazy<T>costs ~136 B (Lazy object + LazyHelper + Func delegate). These are "lazy" but still allocate 3 objects each upfront. Consider replacing with null-check patterns in single-threaded UI paths.Delegate objects (192 B, 6%) — eagerly subscribed during construction. Consider deferring subscriptions where safe.
NavigationProxy (40 B) + BindablePropertyContext (136 B) —
NavigableElementctor creates aNavigationProxy+ fullBindablePropertyContextviaSetValue. Most views never use direct navigation.