diff --git a/Strand/Liquid/LiquidTodayView.swift b/Strand/Liquid/LiquidTodayView.swift index 7179ee9ba7..c6a775c3ed 100644 --- a/Strand/Liquid/LiquidTodayView.swift +++ b/Strand/Liquid/LiquidTodayView.swift @@ -343,7 +343,8 @@ struct LiquidTodayView: View { liquidRefreshIndicator // grows in the revealed space; a vessel filling with the pull - VStack(alignment: .leading, spacing: 12) { + // Materialise charts below the viewport only when they approach the visible area. + LazyVStack(alignment: .leading, spacing: NoopMetrics.space3) { scene // The strain/illness early-warning banner, dropped in the liquid Home rewrite. Liquid is // the DEFAULT Today on both platforms (RootTabView.swift's liquidTodayEnabled = true, diff --git a/Strand/Screens/TodayView.swift b/Strand/Screens/TodayView.swift index 4935abfd9c..992cab01af 100644 --- a/Strand/Screens/TodayView.swift +++ b/Strand/Screens/TodayView.swift @@ -1455,10 +1455,7 @@ struct TodayView: View { var body: some View { ScreenScaffold(title: scaffoldTitle, onRefresh: { await repo.refresh() }, - // PERF (scroll): lazy column so the scaffold materialises Today's content on demand. - // Today supplies its own inner eager VStack (below), so the staggered section reveal is - // unchanged, this only defers building the single inner stack until it scrolls in. - // Byte-identical layout (LazyVStack == eager VStack alignment/spacing/header). + // Both columns are lazy so off-screen sections defer chart layout. lazy: true, // PERF (scroll stutter): the day-cycle scene is a static masked Image. CoreAnimation // already caches it as a stable image layer, so it does NOT re-rasterize on body @@ -1468,7 +1465,7 @@ struct TodayView: View { // lag regression; removing the flatten restores native layer caching. topBackground: showDayCycleBackground ? AnyView(SceneScreenBackground(hour: demoSceneHour)) : nil) { - VStack(alignment: .leading, spacing: NoopMetrics.sectionGap) { + LazyVStack(alignment: .leading, spacing: NoopMetrics.sectionGap) { #if os(iOS) // Compact top bar: profile/settings (left) · ‹ Today › day-nav (centre, bold) · strap // battery (right). Replaces the big title + the full-width day-nav pill (WHOOP-style). diff --git a/Strand/Screens/TrendsView.swift b/Strand/Screens/TrendsView.swift index 8bf18e41b2..781cc2d4eb 100644 --- a/Strand/Screens/TrendsView.swift +++ b/Strand/Screens/TrendsView.swift @@ -291,9 +291,7 @@ struct TrendsView: View { private var scaffold: some View { ScreenScaffold(title: "Trends", subtitle: "The thread of you over time.", - // PERF (scroll): lazy column — byte-identical layout (LazyVStack == eager VStack - // alignment/spacing/header). The content is one inner eager VStack, so the staggered - // section reveal is unchanged; this only defers building that stack until it scrolls in. + // Defer off-screen charts in the inner column as well as the scaffold. onRefresh: { await repo.refresh() }, lazy: true, topBackground: liquidScaffoldSky()) { @@ -305,31 +303,23 @@ struct TrendsView: View { // Reuse the resolved windows until the data, range, or loaded Rest series changes. // An unrelated Repository publication must not re-filter five years of history. let metrics = resolvedMetrics - VStack(alignment: .leading, spacing: NoopMetrics.sectionSpacing) { - // The main card list ripples in once on appear (Reduce-Motion safe). + LazyVStack(alignment: .leading, spacing: NoopMetrics.sectionSpacing) { + // Charts enter directly; lazy scrolling must not schedule staggered layout animations. Group { // Week-in-review digest (#208) with prev/next week browsing (#710) — self-hides // only when NO week in history has data. Past weeks render in the same format. weeklyDigestNav - .staggeredAppear(index: 0) // The Charge / Effort / Rest trio, presented in NOOP's pip language. weekInReview(charge: metrics.recovery, effort: metrics.strain, rest: metrics.rest) - .staggeredAppear(index: 1) rangeBar(recovery: metrics.recovery) - .staggeredAppear(index: 2) heroRecovery(recovery: metrics.recovery) - .staggeredAppear(index: 3) smallMultiples(hrv: metrics.hrv, rhr: metrics.rhr, strain: metrics.strain) - .staggeredAppear(index: 4) // Long-horizon training load (CTL/ATL/TSB). Uses the FULL history, not the // range window — chronic load is inherently a 42-day horizon. Self-hides its // chart behind an honest "needs N more days" state until enough history exists. TrainingLoadCard(days: repo.days) - .staggeredAppear(index: 5) yearStrip - .staggeredAppear(index: 6) exportReportRow - .staggeredAppear(index: 7) } } }