From 56826c650e57a468f2146e0bf1bcb31d505a4632 Mon Sep 17 00:00:00 2001 From: AlexSchmidt1999 <136347876+AlexSchmidt1999@users.noreply.github.com> Date: Fri, 2 Oct 2026 14:47:03 +0200 Subject: [PATCH 1/2] perf: build dashboard sections lazily inside the scroll content --- Strand/Liquid/LiquidTodayView.swift | 5 ++++- Strand/Screens/InsightsHubView.swift | 6 ++---- Strand/Screens/InsightsView.swift | 6 ++---- Strand/Screens/TodayView.swift | 8 +++----- Strand/Screens/TrendsView.swift | 7 +++---- 5 files changed, 14 insertions(+), 18 deletions(-) diff --git a/Strand/Liquid/LiquidTodayView.swift b/Strand/Liquid/LiquidTodayView.swift index 80948dec8c..2e8d3b228f 100644 --- a/Strand/Liquid/LiquidTodayView.swift +++ b/Strand/Liquid/LiquidTodayView.swift @@ -343,7 +343,10 @@ struct LiquidTodayView: View { liquidRefreshIndicator // grows in the revealed space; a vessel filling with the pull - VStack(alignment: .leading, spacing: 12) { + // An eager inner stack constructs the entire dashboard even inside a lazy scroll + // scaffold. Defer individual sections; keep the pull probe outside so refresh still + // observes the top offset before any section has been materialized. + 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/InsightsHubView.swift b/Strand/Screens/InsightsHubView.swift index a5fe185a22..114ed9a9fb 100644 --- a/Strand/Screens/InsightsHubView.swift +++ b/Strand/Screens/InsightsHubView.swift @@ -40,14 +40,12 @@ struct InsightsHubView: View { var body: some View { ScreenScaffold(title: "Insights", subtitle: "Patterns in your own data: association, not cause.", - // PERF (scroll): lazy column — byte-identical layout (LazyVStack == eager VStack - // alignment/spacing/header). The content is one inner eager VStack, so the staggered - // mover reveal is unchanged; this only defers building that stack until it scrolls in. + // The inner lazy column below builds each section as it enters the viewport. lazy: true) { if !model.loaded { ComingSoon(what: "Reading your journal and outcomes…") } else { - VStack(alignment: .leading, spacing: NoopMetrics.sectionSpacing) { + LazyVStack(alignment: .leading, spacing: NoopMetrics.sectionSpacing) { moversSection doseSection methodNote diff --git a/Strand/Screens/InsightsView.swift b/Strand/Screens/InsightsView.swift index 7d455bdcce..a03de2fd06 100644 --- a/Strand/Screens/InsightsView.swift +++ b/Strand/Screens/InsightsView.swift @@ -210,9 +210,7 @@ struct InsightsView: View { var body: some View { ScreenScaffold(title: "Insights", subtitle: "Interrogate what affects what.", - // PERF (scroll): lazy column, byte-identical layout (LazyVStack == eager VStack - // alignment/spacing/header). The content is one inner eager VStack, so any nested - // staggered reveals are unchanged; this only defers building that stack on scroll-in. + // The inner lazy column below builds each section as it enters the viewport. lazy: true, // Liquid finish: the same full-bleed day-of-sky backdrop Today + the other liquid // tabs carry, so Insights sits in one atmosphere ("the options change, not the page"). @@ -221,7 +219,7 @@ struct InsightsView: View { if !loaded { ComingSoon(what: "Reading your journal and outcomes…") } else { - VStack(alignment: .leading, spacing: NoopMetrics.sectionSpacing) { + LazyVStack(alignment: .leading, spacing: NoopMetrics.sectionSpacing) { // v5: a single row into the "What moves you" hub, the lag-aware ranked-effect feed // + alcohol/caffeine dose-response. Reachable as its own destination too; this is the // honest in-Insights entry point. diff --git a/Strand/Screens/TodayView.swift b/Strand/Screens/TodayView.swift index 43f3a850ea..5987fb5bc8 100644 --- a/Strand/Screens/TodayView.swift +++ b/Strand/Screens/TodayView.swift @@ -1456,10 +1456,8 @@ 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). + // Keep the scaffold's lazy column; the inner lazy stack below also defers each + // off-screen Today section instead of eagerly constructing the whole dashboard. 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 @@ -1469,7 +1467,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 8f68bb601b..d7b32053ec 100644 --- a/Strand/Screens/TrendsView.swift +++ b/Strand/Screens/TrendsView.swift @@ -240,9 +240,8 @@ 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. + // The outer lazy column defers the content, and the inner lazy column below + // defers each chart section as it enters the viewport. onRefresh: { await repo.refresh() }, lazy: true, topBackground: liquidScaffoldSky()) { @@ -262,7 +261,7 @@ struct TrendsView: View { // Rest = the sleep_performance composite — the same number the Today Rest score shows // (#732); see sleepPerfByDay. resolve() still does the windowing/widening. let rest = resolve { sleepPerfByDay[$0.day] } - VStack(alignment: .leading, spacing: NoopMetrics.sectionSpacing) { + LazyVStack(alignment: .leading, spacing: NoopMetrics.sectionSpacing) { // The main card list ripples in once on appear (Reduce-Motion safe). Group { // Week-in-review digest (#208) with prev/next week browsing (#710) — self-hides From db8972925e2b23ac1367aa83bdc3d64a30c658f9 Mon Sep 17 00:00:00 2001 From: alexanderschmidt2 <67632384+alexanderschmidt2@users.noreply.github.com> Date: Fri, 16 Oct 2026 19:52:46 +0200 Subject: [PATCH 2/2] Match lazy dashboard scope to the installed iPhone build --- Strand/Liquid/LiquidTodayView.swift | 4 +--- Strand/Screens/InsightsHubView.swift | 6 ++++-- Strand/Screens/InsightsView.swift | 6 ++++-- Strand/Screens/TodayView.swift | 3 +-- Strand/Screens/TrendsView.swift | 5 ++--- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Strand/Liquid/LiquidTodayView.swift b/Strand/Liquid/LiquidTodayView.swift index 6d8a6d9e76..c6a775c3ed 100644 --- a/Strand/Liquid/LiquidTodayView.swift +++ b/Strand/Liquid/LiquidTodayView.swift @@ -343,9 +343,7 @@ struct LiquidTodayView: View { liquidRefreshIndicator // grows in the revealed space; a vessel filling with the pull - // An eager inner stack constructs the entire dashboard even inside a lazy scroll - // scaffold. Defer individual sections; keep the pull probe outside so refresh still - // observes the top offset before any section has been materialized. + // 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 diff --git a/Strand/Screens/InsightsHubView.swift b/Strand/Screens/InsightsHubView.swift index 114ed9a9fb..a5fe185a22 100644 --- a/Strand/Screens/InsightsHubView.swift +++ b/Strand/Screens/InsightsHubView.swift @@ -40,12 +40,14 @@ struct InsightsHubView: View { var body: some View { ScreenScaffold(title: "Insights", subtitle: "Patterns in your own data: association, not cause.", - // The inner lazy column below builds each section as it enters the viewport. + // PERF (scroll): lazy column — byte-identical layout (LazyVStack == eager VStack + // alignment/spacing/header). The content is one inner eager VStack, so the staggered + // mover reveal is unchanged; this only defers building that stack until it scrolls in. lazy: true) { if !model.loaded { ComingSoon(what: "Reading your journal and outcomes…") } else { - LazyVStack(alignment: .leading, spacing: NoopMetrics.sectionSpacing) { + VStack(alignment: .leading, spacing: NoopMetrics.sectionSpacing) { moversSection doseSection methodNote diff --git a/Strand/Screens/InsightsView.swift b/Strand/Screens/InsightsView.swift index a03de2fd06..7d455bdcce 100644 --- a/Strand/Screens/InsightsView.swift +++ b/Strand/Screens/InsightsView.swift @@ -210,7 +210,9 @@ struct InsightsView: View { var body: some View { ScreenScaffold(title: "Insights", subtitle: "Interrogate what affects what.", - // The inner lazy column below builds each section as it enters the viewport. + // PERF (scroll): lazy column, byte-identical layout (LazyVStack == eager VStack + // alignment/spacing/header). The content is one inner eager VStack, so any nested + // staggered reveals are unchanged; this only defers building that stack on scroll-in. lazy: true, // Liquid finish: the same full-bleed day-of-sky backdrop Today + the other liquid // tabs carry, so Insights sits in one atmosphere ("the options change, not the page"). @@ -219,7 +221,7 @@ struct InsightsView: View { if !loaded { ComingSoon(what: "Reading your journal and outcomes…") } else { - LazyVStack(alignment: .leading, spacing: NoopMetrics.sectionSpacing) { + VStack(alignment: .leading, spacing: NoopMetrics.sectionSpacing) { // v5: a single row into the "What moves you" hub, the lag-aware ranked-effect feed // + alcohol/caffeine dose-response. Reachable as its own destination too; this is the // honest in-Insights entry point. diff --git a/Strand/Screens/TodayView.swift b/Strand/Screens/TodayView.swift index 4e237cfceb..992cab01af 100644 --- a/Strand/Screens/TodayView.swift +++ b/Strand/Screens/TodayView.swift @@ -1455,8 +1455,7 @@ struct TodayView: View { var body: some View { ScreenScaffold(title: scaffoldTitle, onRefresh: { await repo.refresh() }, - // Keep the scaffold's lazy column; the inner lazy stack below also defers each - // off-screen Today section instead of eagerly constructing the whole dashboard. + // 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 diff --git a/Strand/Screens/TrendsView.swift b/Strand/Screens/TrendsView.swift index e38c2abe0b..781cc2d4eb 100644 --- a/Strand/Screens/TrendsView.swift +++ b/Strand/Screens/TrendsView.swift @@ -291,8 +291,7 @@ struct TrendsView: View { private var scaffold: some View { ScreenScaffold(title: "Trends", subtitle: "The thread of you over time.", - // The outer lazy column defers the content, and the inner lazy column below - // defers each chart section as it enters the viewport. + // Defer off-screen charts in the inner column as well as the scaffold. onRefresh: { await repo.refresh() }, lazy: true, topBackground: liquidScaffoldSky()) { @@ -305,7 +304,7 @@ struct TrendsView: View { // An unrelated Repository publication must not re-filter five years of history. let metrics = resolvedMetrics LazyVStack(alignment: .leading, spacing: NoopMetrics.sectionSpacing) { - // Lazy charts appear directly instead of starting a layout animation while scrolling. + // 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.