diff --git a/src/app/insights/mood/page.tsx b/src/app/insights/mood/page.tsx
index 9f0324a08..6e1e08f34 100644
--- a/src/app/insights/mood/page.tsx
+++ b/src/app/insights/mood/page.tsx
@@ -30,7 +30,7 @@ import { apiGet } from "@/lib/api/api-fetch";
*/
const MoodChart = dynamic(
() =>
- import("@/components/charts/mood-chart").then((mod) => ({
+ import("@/components/charts/chart-runtime").then((mod) => ({
default: mod.MoodChart,
})),
{ ssr: false, loading: () => },
diff --git a/src/app/page.tsx b/src/app/page.tsx
index a2154cc1d..04da3b69d 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -81,7 +81,7 @@ const BATCH_COVERAGE_DAYS = 31;
// to ONE error card instead of bubbling to the route-level `error.tsx`.
const MoodChartLazy = dynamic(
() =>
- importWithRetry(() => import("@/components/charts/mood-chart")).then(
+ importWithRetry(() => import("@/components/charts/chart-runtime")).then(
(mod) => ({ default: mod.MoodChart }),
),
{ ssr: false, loading: () => },
@@ -94,7 +94,7 @@ const MoodChart = (props: React.ComponentProps) => (
const MedicationComplianceChartLazy = dynamic(
() =>
importWithRetry(
- () => import("@/components/charts/medication-compliance-chart"),
+ () => import("@/components/charts/chart-runtime"),
).then((mod) => ({ default: mod.MedicationComplianceChart })),
{ ssr: false, loading: () => },
);
diff --git a/src/components/admin/system-status-section.tsx b/src/components/admin/system-status-section.tsx
index 2fff4d5c1..053789cc1 100644
--- a/src/components/admin/system-status-section.tsx
+++ b/src/components/admin/system-status-section.tsx
@@ -32,7 +32,7 @@ import { StatusItem, usePublicVersion, useSystemStatus } from "./_shared";
// working — same pattern the insights page uses for the scatter chart.
const HostMetricsChart = dynamic(
() =>
- import("./host-metrics-chart").then((mod) => ({
+ import("@/components/charts/chart-runtime").then((mod) => ({
default: mod.HostMetricsChart,
})),
{
diff --git a/src/components/charts/chart-runtime.ts b/src/components/charts/chart-runtime.ts
new file mode 100644
index 000000000..f5498f07d
--- /dev/null
+++ b/src/components/charts/chart-runtime.ts
@@ -0,0 +1,43 @@
+/**
+ * The ONE async boundary for every recharts-rendering component.
+ *
+ * Every `next/dynamic` chart import in the tree points at THIS module
+ * (`import("@/components/charts/chart-runtime")`), never at the individual
+ * chart files. Pointing N dynamic boundaries at N different modules made
+ * Turbopack emit a separate chunk group per boundary, each carrying its own
+ * full copy of the recharts module graph — the production build shipped
+ * EIGHT ~312 KB copies of the library, and /insights alone downloaded two
+ * (~92 KB gz of pure duplication on the worst-LCP route). One shared import
+ * target ⇒ one shared chunk group ⇒ recharts exists exactly once, cached
+ * across every route.
+ *
+ * Extends the v1.16.7 `mood-charts.ts` precedent (which did this for the
+ * mood trio) to the whole chart surface; that scoped barrel is folded in
+ * here.
+ *
+ * Rules:
+ * - A new recharts consumer is added HERE and loaded through
+ * `dynamic(() => import("@/components/charts/chart-runtime").then(...))`
+ * at its call site — with the call site's own matching skeleton.
+ * - No component outside this module's static import graph may import
+ * "recharts" directly.
+ * - Types stay importable from the component files directly (type-only
+ * imports are value-free and don't drag the chunk in).
+ */
+
+export { HealthChart } from "./health-chart";
+export { MoodChart } from "./mood-chart";
+export { MedicationComplianceChart } from "./medication-compliance-chart";
+export { ScatterCorrelationChart } from "./scatter-correlation-chart";
+export { HostMetricsChart } from "@/components/admin/host-metrics-chart";
+export { CustomMetricChart } from "@/components/custom-metrics/custom-metric-chart";
+export { BbtChart } from "@/components/cycle/bbt-chart";
+export { DeltaSparkline } from "@/components/insights/derived/delta-sparkline";
+export { MoodDistributionChart } from "@/components/insights/mood/mood-distribution-chart";
+export { MoodTimeOfDayChart } from "@/components/insights/mood/mood-time-of-day-chart";
+export { MoodWeekdayChart } from "@/components/insights/mood/mood-weekday-chart";
+export { SleepStageStackedBar } from "@/components/insights/sleep-stage-stacked-bar";
+export { LabBiomarkerChart } from "@/components/labs/lab-biomarker-chart";
+export { DoseStrengthCurve } from "@/components/medications/dose-strength-curve";
+export { DrugLevelChart } from "@/components/medications/drug-level-chart";
+export { AssessmentHistoryChart } from "@/components/mental-health/assessment-history-chart";
diff --git a/src/components/charts/health-chart-dynamic.tsx b/src/components/charts/health-chart-dynamic.tsx
index 06c92e7b2..9a5f37d72 100644
--- a/src/components/charts/health-chart-dynamic.tsx
+++ b/src/components/charts/health-chart-dynamic.tsx
@@ -14,7 +14,7 @@ import { importWithRetry } from "@/lib/retry-import";
* Pre-fix, eight call sites (`src/app/page.tsx`, five insights
* sub-pages, `sleep-duration-chart.tsx`, `trends-row.tsx`, and
* `vo2-max-chart-row.tsx`) duplicated the same
- * `dynamic(() => import("@/components/charts/health-chart"), { ... })`
+ * `dynamic(() => import("@/components/charts/chart-runtime"), { ... })`
* incantation. The duplication meant the loading skeleton + `ssr: false`
* flag had to be re-wired on every caller.
*
@@ -31,7 +31,7 @@ import { importWithRetry } from "@/lib/retry-import";
*/
const HealthChartLazy = dynamic(
() =>
- importWithRetry(() => import("@/components/charts/health-chart")).then(
+ importWithRetry(() => import("@/components/charts/chart-runtime")).then(
(mod) => ({ default: mod.HealthChart }),
),
{ ssr: false, loading: () => },
@@ -59,7 +59,7 @@ export function HealthChartDynamic(
*/
const HealthChartLazyMini = dynamic(
() =>
- importWithRetry(() => import("@/components/charts/health-chart")).then(
+ importWithRetry(() => import("@/components/charts/chart-runtime")).then(
(mod) => ({ default: mod.HealthChart }),
),
{ ssr: false, loading: () => },
diff --git a/src/components/custom-metrics/custom-metric-detail.tsx b/src/components/custom-metrics/custom-metric-detail.tsx
index 9caf25593..25d6960aa 100644
--- a/src/components/custom-metrics/custom-metric-detail.tsx
+++ b/src/components/custom-metrics/custom-metric-detail.tsx
@@ -42,7 +42,7 @@ import type {
// off the detail page's first-load JS; the skeleton shell holds the layout.
const CustomMetricChartLazy = dynamic(
() =>
- importWithRetry(() => import("./custom-metric-chart")).then((mod) => ({
+ importWithRetry(() => import("@/components/charts/chart-runtime")).then((mod) => ({
default: mod.CustomMetricChart,
})),
{ ssr: false, loading: () => },
diff --git a/src/components/cycle/cycle-view.tsx b/src/components/cycle/cycle-view.tsx
index 0f9bbfe2b..8665fe1e4 100644
--- a/src/components/cycle/cycle-view.tsx
+++ b/src/components/cycle/cycle-view.tsx
@@ -1,17 +1,18 @@
"use client";
import { useMemo, useState } from "react";
+import dynamic from "next/dynamic";
import { useSearchParams } from "next/navigation";
import { Loader2, Plus, RefreshCw, Sparkles } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { PageHeader } from "@/components/ui/page-header";
+import { Skeleton } from "@/components/ui/skeleton";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { useTranslations } from "@/lib/i18n/context";
import { cn } from "@/lib/utils";
import { CycleRing } from "./cycle-ring";
-import { BbtChart } from "./bbt-chart";
import { CycleCalendar } from "./cycle-calendar";
import { CycleHistoryChart } from "./cycle-history-chart";
import { LogDaySheet } from "./log-day-sheet";
@@ -54,6 +55,33 @@ function shiftToday(days: number): string {
/** The four cycle tabs, in render order. The deep-link value is validated
* against this set so a junk `?tab=` falls back to the calendar tab. */
+// The BBT chart is the only recharts consumer on /cycle — load it through
+// the shared chart-runtime boundary so the route stops carrying its own
+// copy of the recharts module graph. The loading shell mirrors the card
+// the chart paints (header row + caption + 210 px chart band inside the
+// standard card chrome) so the tab layout holds still while the shared
+// chunk streams.
+const BbtChart = dynamic(
+ () =>
+ import("@/components/charts/chart-runtime").then((mod) => ({
+ default: mod.BbtChart,
+ })),
+ {
+ ssr: false,
+ loading: () => (
+
+ ),
+ },
+);
+
const CYCLE_TABS = ["calendar", "predictions", "insights", "settings"] as const;
type CycleTab = (typeof CYCLE_TABS)[number];
diff --git a/src/components/insights/__tests__/recommendation-card.test.tsx b/src/components/insights/__tests__/recommendation-card.test.tsx
index 1fddb563d..6e7ab768e 100644
--- a/src/components/insights/__tests__/recommendation-card.test.tsx
+++ b/src/components/insights/__tests__/recommendation-card.test.tsx
@@ -47,10 +47,12 @@ vi.mock("next/dynamic", async () => {
const mood = await import("@/components/charts/mood-chart");
return {
default: (loader: unknown) => {
- // Identify which chart the loader targets by its source text.
+ // Identify which chart the loader targets by its source text. Every
+ // loader now resolves the shared chart-runtime boundary and picks a
+ // named export off it, so the export name is the discriminator.
const src = String(loader);
const Comp = (
- src.includes("mood-chart") ? mood.MoodChart : health.HealthChart
+ src.includes("MoodChart") ? mood.MoodChart : health.HealthChart
) as React.ComponentType>;
return (props: Record) => ;
},
diff --git a/src/components/insights/correlation-card.tsx b/src/components/insights/correlation-card.tsx
index 12dd518ec..f42e00b87 100644
--- a/src/components/insights/correlation-card.tsx
+++ b/src/components/insights/correlation-card.tsx
@@ -29,7 +29,7 @@ import { AskCoachAction } from "./ask-coach-action";
const ScatterCorrelationChart = dynamic(
() =>
- import("@/components/charts/scatter-correlation-chart").then((mod) => ({
+ import("@/components/charts/chart-runtime").then((mod) => ({
default: mod.ScatterCorrelationChart,
})),
{
diff --git a/src/components/insights/derived/delta-sparkline.tsx b/src/components/insights/derived/delta-sparkline.tsx
new file mode 100644
index 000000000..7d9500ce0
--- /dev/null
+++ b/src/components/insights/derived/delta-sparkline.tsx
@@ -0,0 +1,49 @@
+"use client";
+
+import { useId } from "react";
+import { Area, AreaChart, ResponsiveContainer, YAxis } from "recharts";
+
+/**
+ * The recharts body of the `` inline sparkline,
+ * extracted so the tile itself carries no static recharts import. The tile
+ * loads this through the shared chart-runtime boundary
+ * (`@/components/charts/chart-runtime`); its fixed 40 px container is owned
+ * by the tile, so the async gap paints an empty band of identical size —
+ * zero layout shift, pixel-identical once mounted.
+ */
+export function DeltaSparkline({
+ data,
+ strokeVar,
+}: {
+ /** `{ i, v }` points, already length-guarded (≥2) by the tile. */
+ data: Array<{ i: number; v: number }>;
+ /** CSS var expression for the sentiment stroke, e.g. `var(--success)`. */
+ strokeVar: string;
+}) {
+ // A stable per-instance id for the gradient . Deriving it from a
+ // label slug collides when two tiles share a localized label (the gradient
+ // fill on the second tile would not resolve); useId() is collision-free.
+ const fillId = useId();
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/insights/derived/sparkline-delta-tile.tsx b/src/components/insights/derived/sparkline-delta-tile.tsx
index 5e249fe08..f6a3467eb 100644
--- a/src/components/insights/derived/sparkline-delta-tile.tsx
+++ b/src/components/insights/derived/sparkline-delta-tile.tsx
@@ -1,8 +1,7 @@
"use client";
-import { useId } from "react";
+import dynamic from "next/dynamic";
import { ArrowDown, ArrowRight, ArrowUp, Minus } from "lucide-react";
-import { Area, AreaChart, ResponsiveContainer, YAxis } from "recharts";
import { cn } from "@/lib/utils";
import { useTranslations, useFormatters } from "@/lib/i18n/context";
import {
@@ -28,6 +27,19 @@ import {
* Fixed footprint so a tile never reflows its neighbours.
*/
+// The recharts sparkline body loads through the shared chart-runtime
+// boundary so this tile (statically imported by the vitals dashboard)
+// carries no recharts in its own chunk group. The tile owns the fixed
+// 40 px container, so the async gap is an identically-sized empty band —
+// no layout shift, no skeleton needed.
+const DeltaSparkline = dynamic(
+ () =>
+ import("@/components/charts/chart-runtime").then((mod) => ({
+ default: mod.DeltaSparkline,
+ })),
+ { ssr: false, loading: () => null },
+);
+
export interface SparklineDeltaTileProps {
label: string;
/** Latest reading; `null` renders the empty state. */
@@ -88,10 +100,6 @@ export function SparklineDeltaTile({
}: SparklineDeltaTileProps) {
const { t } = useTranslations();
const fmt = useFormatters();
- // A stable per-instance id for the gradient . Deriving it from the
- // label slug collides when two tiles share a localized label (the gradient
- // fill on the second tile would not resolve); useId() is collision-free.
- const fillId = useId();
const arrowSentiment = getTrendSentiment(delta ?? null, directionSentiment);
const trendColor = sentimentColorClass(arrowSentiment);
@@ -190,29 +198,7 @@ export function SparklineDeltaTile({
className="mt-3 h-10 w-full"
data-slot="sparkline-delta-tile-spark"
>
-
-
-
-
-
-
-
-
-
-
-
-
+
) : null}
diff --git a/src/components/insights/mood/mood-charts.ts b/src/components/insights/mood/mood-charts.ts
deleted file mode 100644
index a1da1f78d..000000000
--- a/src/components/insights/mood/mood-charts.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-/**
- * v1.16.7 — single async boundary for the three mood mini-charts.
- *
- * The distribution / weekday / time-of-day charts were each deferred
- * through their own `next/dynamic` import, which produced three separate
- * chunk groups: the cards popped in one after another as each chunk
- * landed (incoherent reveal on the mood insights page), and each group
- * carried its own copy of the Recharts module graph. Funnelling all
- * three through this one barrel gives them a single shared chunk — they
- * arrive (and reveal) together, and Recharts is bundled once for the
- * trio.
- *
- * Consumers keep importing the chart types directly from the component
- * files (type-only imports are value-free and don't drag the chunk in).
- */
-export { MoodDistributionChart } from "./mood-distribution-chart";
-export { MoodWeekdayChart } from "./mood-weekday-chart";
-export { MoodTimeOfDayChart } from "./mood-time-of-day-chart";
diff --git a/src/components/insights/mood/mood-correlation-cards.tsx b/src/components/insights/mood/mood-correlation-cards.tsx
index bb3bd5e9c..062f9fa1d 100644
--- a/src/components/insights/mood/mood-correlation-cards.tsx
+++ b/src/components/insights/mood/mood-correlation-cards.tsx
@@ -25,7 +25,7 @@ import { MoodExplainerIcon } from "./mood-explainer-icon";
const ScatterCorrelationChart = dynamic(
() =>
- import("@/components/charts/scatter-correlation-chart").then((mod) => ({
+ import("@/components/charts/chart-runtime").then((mod) => ({
default: mod.ScatterCorrelationChart,
})),
{
diff --git a/src/components/insights/mood/mood-insights-sections.tsx b/src/components/insights/mood/mood-insights-sections.tsx
index 5fe0d9385..19a5156e0 100644
--- a/src/components/insights/mood/mood-insights-sections.tsx
+++ b/src/components/insights/mood/mood-insights-sections.tsx
@@ -36,12 +36,13 @@ import { MoodHeatmap } from "@/components/charts/mood-heatmap";
import type { MoodDistributionRow } from "./mood-distribution-chart";
import type { MoodWeekdayRow } from "./mood-weekday-chart";
import type { MoodTimeOfDayPattern } from "./mood-time-of-day-chart";
-// v1.16.7 — all three loaders resolve the ONE `./mood-charts` barrel so
-// they share a single chunk: the cards reveal together instead of
-// popping in chunk-by-chunk, and Recharts ships once for the trio.
+// v1.16.7 established the shared-barrel pattern for the trio; all three
+// loaders now resolve the app-wide chart-runtime boundary, so they share
+// the ONE recharts chunk with every other chart surface: the cards reveal
+// together, and recharts ships exactly once for the whole app.
const MoodDistributionChart = dynamic(
() =>
- import("./mood-charts").then((mod) => ({
+ import("@/components/charts/chart-runtime").then((mod) => ({
default: mod.MoodDistributionChart,
})),
{
@@ -53,7 +54,7 @@ const MoodDistributionChart = dynamic(
);
const MoodWeekdayChart = dynamic(
() =>
- import("./mood-charts").then((mod) => ({
+ import("@/components/charts/chart-runtime").then((mod) => ({
default: mod.MoodWeekdayChart,
})),
{
@@ -65,7 +66,7 @@ const MoodWeekdayChart = dynamic(
);
const MoodTimeOfDayChart = dynamic(
() =>
- import("./mood-charts").then((mod) => ({
+ import("@/components/charts/chart-runtime").then((mod) => ({
default: mod.MoodTimeOfDayChart,
})),
{
diff --git a/src/components/insights/recommendation-card.tsx b/src/components/insights/recommendation-card.tsx
index f114b637a..f777b9daf 100644
--- a/src/components/insights/recommendation-card.tsx
+++ b/src/components/insights/recommendation-card.tsx
@@ -35,7 +35,7 @@ import { ConfidenceMeter } from "./confidence-meter";
*/
const HealthChartLazy = dynamic(
() =>
- importWithRetry(() => import("@/components/charts/health-chart")).then(
+ importWithRetry(() => import("@/components/charts/chart-runtime")).then(
(mod) => ({ default: mod.HealthChart }),
),
{ ssr: false, loading: () => },
@@ -48,7 +48,7 @@ const HealthChart = (props: ComponentProps) => (
const MoodChartLazy = dynamic(
() =>
- importWithRetry(() => import("@/components/charts/mood-chart")).then(
+ importWithRetry(() => import("@/components/charts/chart-runtime")).then(
(mod) => ({ default: mod.MoodChart }),
),
{ ssr: false, loading: () => },
diff --git a/src/components/insights/sleep-overview.tsx b/src/components/insights/sleep-overview.tsx
index aaf123bcf..4c0c412f2 100644
--- a/src/components/insights/sleep-overview.tsx
+++ b/src/components/insights/sleep-overview.tsx
@@ -25,7 +25,7 @@ import { SleepDurationChart } from "./sleep-duration-chart";
// matches the card the bar paints so the layout stays stable.
const SleepStageStackedBarLazy = dynamic(
() =>
- importWithRetry(() => import("./sleep-stage-stacked-bar")).then((mod) => ({
+ importWithRetry(() => import("@/components/charts/chart-runtime")).then((mod) => ({
default: mod.SleepStageStackedBar,
})),
{ ssr: false, loading: () => },
diff --git a/src/components/insights/trends-row.tsx b/src/components/insights/trends-row.tsx
index 26a48a6bc..a5f643745 100644
--- a/src/components/insights/trends-row.tsx
+++ b/src/components/insights/trends-row.tsx
@@ -58,7 +58,7 @@ import {
const MoodChart = dynamic(
() =>
- import("@/components/charts/mood-chart").then((mod) => ({
+ import("@/components/charts/chart-runtime").then((mod) => ({
default: mod.MoodChart,
})),
{ ssr: false, loading: () => },
diff --git a/src/components/labs/lab-biomarker-detail.tsx b/src/components/labs/lab-biomarker-detail.tsx
index 329bd622b..cb2162230 100644
--- a/src/components/labs/lab-biomarker-detail.tsx
+++ b/src/components/labs/lab-biomarker-detail.tsx
@@ -51,7 +51,7 @@ import type {
// loading shell matches the in-card chart footprint so the layout is stable.
const LabBiomarkerChartLazy = dynamic(
() =>
- importWithRetry(() => import("./lab-biomarker-chart")).then((mod) => ({
+ importWithRetry(() => import("@/components/charts/chart-runtime")).then((mod) => ({
default: mod.LabBiomarkerChart,
})),
{ ssr: false, loading: () => },
diff --git a/src/components/medications/detail/medication-detail-tabs.tsx b/src/components/medications/detail/medication-detail-tabs.tsx
index 83a7240ff..f9a803ae7 100644
--- a/src/components/medications/detail/medication-detail-tabs.tsx
+++ b/src/components/medications/detail/medication-detail-tabs.tsx
@@ -89,14 +89,14 @@ import type { ComplianceDisplay } from "@/lib/analytics/compliance";
// shared chart skeleton standing in while the chunk streams.
const DrugLevelChart = dynamic(
() =>
- import("@/components/medications/drug-level-chart").then((mod) => ({
+ import("@/components/charts/chart-runtime").then((mod) => ({
default: mod.DrugLevelChart,
})),
{ ssr: false, loading: () => },
);
const DoseStrengthCurve = dynamic(
() =>
- import("@/components/medications/dose-strength-curve").then((mod) => ({
+ import("@/components/charts/chart-runtime").then((mod) => ({
default: mod.DoseStrengthCurve,
})),
{ ssr: false, loading: () => },
diff --git a/src/components/mental-health/assessment-history.tsx b/src/components/mental-health/assessment-history.tsx
index 570e4c568..094d572f4 100644
--- a/src/components/mental-health/assessment-history.tsx
+++ b/src/components/mental-health/assessment-history.tsx
@@ -38,7 +38,7 @@ import type { AssessmentRow, CrisisSet, InstrumentId } from "./types";
// holds the layout.
const AssessmentHistoryChartLazy = dynamic(
() =>
- importWithRetry(() => import("./assessment-history-chart")).then((mod) => ({
+ importWithRetry(() => import("@/components/charts/chart-runtime")).then((mod) => ({
default: mod.AssessmentHistoryChart,
})),
{ ssr: false, loading: () => },