Scrolling the Dashboard quickly stutters, most visibly when the pointer passes over the usage charts.
Root causes
- Hover-selection storm. Both charts in
Zerm/Views/Metrics/UsageTrendCharts.swift bind .chartXSelection(value: $selectedDate). On macOS this tracks the pointer, so content scrolling under a stationary cursor rewrites selectedDate on every event. Each write re-evaluates the whole view body and rebuilds both Charts.
- O(n^2) inside the mark builders.
selectedBucket is a computed property doing buckets.min { ... } (O(n)) and it is referenced inside the Chart(buckets) { bucket in ... } closures, so it runs once per data element. ratePoints, peakBucket, lastRateBucket and xDomain are likewise recomputed on every body pass.
- Per-frame compositing.
.thinMaterial on six MetricCards, both chart cards and the footer capsule, plus a .shadow(radius: 30) on the hero — each an offscreen blur pass on every scroll frame.
- Non-lazy data table.
UsageDataTable lays out every row in a plain VStack inside the outer ScrollView; on All Time that is one row per active day.
- Reload churn.
MetricsContent.reload() re-fetches the entire UsageDay store on the main actor for every .usageStatsUpdated post and every range switch, even though the range window is applied in memory afterwards.
Fix
- Precompute everything derived from
buckets into an Equatable series value built once, and look up the nearest bucket by index instead of min(by:).
- Snap chart selection to a bucket and write state only when the selected bucket actually changes, so pointer movement inside one bar costs nothing.
- Hoist selection checks out of the mark builders so the marks are selection-invariant.
- Replace card materials with a solid adaptive fill plus hairline border, and shrink the hero shadow.
LazyVStack for the data table.
- Cache the fetched days, re-bucket in memory on range change, and coalesce
.usageStatsUpdated reloads.
No behaviour or number on screen changes.
Scrolling the Dashboard quickly stutters, most visibly when the pointer passes over the usage charts.
Root causes
Zerm/Views/Metrics/UsageTrendCharts.swiftbind.chartXSelection(value: $selectedDate). On macOS this tracks the pointer, so content scrolling under a stationary cursor rewritesselectedDateon every event. Each write re-evaluates the whole view body and rebuilds bothCharts.selectedBucketis a computed property doingbuckets.min { ... }(O(n)) and it is referenced inside theChart(buckets) { bucket in ... }closures, so it runs once per data element.ratePoints,peakBucket,lastRateBucketandxDomainare likewise recomputed on every body pass..thinMaterialon sixMetricCards, both chart cards and the footer capsule, plus a.shadow(radius: 30)on the hero — each an offscreen blur pass on every scroll frame.UsageDataTablelays out every row in a plainVStackinside the outerScrollView; on All Time that is one row per active day.MetricsContent.reload()re-fetches the entireUsageDaystore on the main actor for every.usageStatsUpdatedpost and every range switch, even though the range window is applied in memory afterwards.Fix
bucketsinto anEquatableseries value built once, and look up the nearest bucket by index instead ofmin(by:).LazyVStackfor the data table..usageStatsUpdatedreloads.No behaviour or number on screen changes.