Skip to content

Commit 95d291f

Browse files
committed
perf(bundle): defer below-fold marketing sections + admin charts via next/dynamic
Closes #87. apps/marketing — Hero stays static (LCP anchor); SocialProof, Features, HowItWorks, Pricing, FAQ, CTA, StickyCTA load via next/dynamic with SSR enabled so markup ships server-rendered but each section's GSAP + ScrollTrigger code splits into its own chunk and loads on idle. apps/admin — GrowthArea + CountBar are recharts-only (~60kb gz), browser-only by nature. Switched to next/dynamic with ssr:false + animated skeleton fallback so the chart engine never blocks first byte.
1 parent 7bc901a commit 95d291f

2 files changed

Lines changed: 74 additions & 9 deletions

File tree

apps/admin/src/app/[locale]/page.tsx

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,40 @@ import {
77
CardTitle,
88
} from "@starter-saas/ui/components/card";
99
import { Building2, CreditCard, TrendingUp, Users } from "lucide-react";
10-
import { GrowthArea } from "@/components/charts/area-chart";
11-
import { CountBar } from "@/components/charts/bar-chart";
10+
import dynamic from "next/dynamic";
1211
import { PageHeader } from "@/components/layout/page-header";
12+
13+
// Recharts is ~60kb gz and only runs in the browser — defer it off the
14+
// critical path so first-byte for admins doesn't ship the chart engine.
15+
const GrowthArea = dynamic(
16+
() =>
17+
import("@/components/charts/area-chart").then((m) => ({
18+
default: m.GrowthArea,
19+
})),
20+
{
21+
ssr: false,
22+
loading: () => <ChartSkeleton />,
23+
},
24+
);
25+
const CountBar = dynamic(
26+
() =>
27+
import("@/components/charts/bar-chart").then((m) => ({
28+
default: m.CountBar,
29+
})),
30+
{
31+
ssr: false,
32+
loading: () => <ChartSkeleton />,
33+
},
34+
);
35+
36+
function ChartSkeleton() {
37+
return (
38+
<div
39+
aria-hidden
40+
className="h-[260px] w-full animate-pulse rounded-md bg-muted/40"
41+
/>
42+
);
43+
}
1344
import {
1445
fetchAuditByAction,
1546
fetchKpis,

apps/marketing/src/app/[locale]/page.tsx

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,47 @@
1+
import dynamic from "next/dynamic";
12
import { MarketingFooter } from "@/components/marketing/footer";
23
import { MarketingHeader } from "@/components/marketing/header";
3-
import { CTA } from "@/components/marketing/sections/cta";
4-
import { FAQ } from "@/components/marketing/sections/faq";
5-
import { Features } from "@/components/marketing/sections/features";
64
import { Hero } from "@/components/marketing/sections/hero";
7-
import { HowItWorks } from "@/components/marketing/sections/how-it-works";
8-
import { Pricing } from "@/components/marketing/sections/pricing";
9-
import { SocialProof } from "@/components/marketing/sections/social-proof";
10-
import { StickyCTA } from "@/components/marketing/sticky-cta";
5+
6+
// Hero stays static — above the fold, anchors LCP. Below-fold sections
7+
// each pull GSAP + ScrollTrigger (~41kb gz) and don't paint until the
8+
// user scrolls; lazy them with SSR so the markup ships server-rendered
9+
// (for SEO + no-JS) but the gsap bundle defers to idle.
10+
const SocialProof = dynamic(() =>
11+
import("@/components/marketing/sections/social-proof").then((m) => ({
12+
default: m.SocialProof,
13+
})),
14+
);
15+
const Features = dynamic(() =>
16+
import("@/components/marketing/sections/features").then((m) => ({
17+
default: m.Features,
18+
})),
19+
);
20+
const HowItWorks = dynamic(() =>
21+
import("@/components/marketing/sections/how-it-works").then((m) => ({
22+
default: m.HowItWorks,
23+
})),
24+
);
25+
const Pricing = dynamic(() =>
26+
import("@/components/marketing/sections/pricing").then((m) => ({
27+
default: m.Pricing,
28+
})),
29+
);
30+
const FAQ = dynamic(() =>
31+
import("@/components/marketing/sections/faq").then((m) => ({
32+
default: m.FAQ,
33+
})),
34+
);
35+
const CTA = dynamic(() =>
36+
import("@/components/marketing/sections/cta").then((m) => ({
37+
default: m.CTA,
38+
})),
39+
);
40+
const StickyCTA = dynamic(() =>
41+
import("@/components/marketing/sticky-cta").then((m) => ({
42+
default: m.StickyCTA,
43+
})),
44+
);
1145

1246
export default function HomePage() {
1347
return (

0 commit comments

Comments
 (0)