From 40bad47d6dea65083c47114bdf3c0342c90f6db3 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 30 Jul 2026 12:33:41 +0000 Subject: [PATCH] fix(kdebek): split home page into per-section islands instead of one client:load blob pages/index.astro hydrated the entire home page (Navbar + Hero + Testimonials + About + Offer + OnlineBookingCta + Footer) as a single client:load React island, shipping ~83KB of React/Astro runtime JS even for sections that never use hooks or handlers. This was the main driver of a 4.2s LCP. Rewrite About, Offer, and OfferPanel as plain .astro components (zero JS) and compose the home page directly in a new home.astro entry point. Only the genuinely interactive pieces now hydrate as islands: Navbar (client:load, above the fold) and AboutGallery/Counter/OfferWidget (client:visible, below the fold). Hero, Testimonials, ServicesGrid, BookingWidget, OpeningHours, OnlineBookingCta, and Footer render as static markup with no client JS. --- apps/kdebek/TODO.md | 104 +++++++------ apps/kdebek/src/modules/home/index.ts | 7 +- .../src/modules/home/presentation/about.astro | 147 ++++++++++++++++++ .../src/modules/home/presentation/about.tsx | 146 ----------------- .../src/modules/home/presentation/home.astro | 27 ++++ .../src/modules/home/presentation/main.tsx | 27 ---- .../home/presentation/offer-panel.astro | 60 +++++++ .../modules/home/presentation/offer-panel.tsx | 58 ------- .../src/modules/home/presentation/offer.astro | 9 ++ .../src/modules/home/presentation/offer.tsx | 9 -- apps/kdebek/src/pages/index.astro | 5 +- 11 files changed, 304 insertions(+), 295 deletions(-) create mode 100644 apps/kdebek/src/modules/home/presentation/about.astro delete mode 100644 apps/kdebek/src/modules/home/presentation/about.tsx create mode 100644 apps/kdebek/src/modules/home/presentation/home.astro delete mode 100644 apps/kdebek/src/modules/home/presentation/main.tsx create mode 100644 apps/kdebek/src/modules/home/presentation/offer-panel.astro delete mode 100644 apps/kdebek/src/modules/home/presentation/offer-panel.tsx create mode 100644 apps/kdebek/src/modules/home/presentation/offer.astro delete mode 100644 apps/kdebek/src/modules/home/presentation/offer.tsx diff --git a/apps/kdebek/TODO.md b/apps/kdebek/TODO.md index 3df079f..7b68ddc 100644 --- a/apps/kdebek/TODO.md +++ b/apps/kdebek/TODO.md @@ -1,49 +1,55 @@ -# TODO - -Follow-ups from the PageSpeed Insights performance audit (mobile, 2026-07-25). -Image sizing, font preload mismatch, and the oversized footer SVG are already fixed. -Remaining items below. - -## Performance - -- [ ] **Stop hydrating the whole page as one `client:load` island.** - `pages/index.astro` mounts `Main` (`modules/home/presentation/main.tsx`) — which - renders Navbar + Hero + Testimonials + About + Offer + OnlineBookingCta + Footer — - as a single eager React tree. This is the main driver of LCP being 4.2s (poor): - it forces ~83KB of React/ReactDOM/jsx-runtime/Astro-renderer JS onto the critical - path competing with the hero image for bandwidth, and produces the "Reduce unused - JavaScript" finding (42% of the 56KB Astro client runtime chunk unused). - - Audited which components actually need client JS (grepped for hooks/handlers): - - Genuinely interactive: `Navbar` (mobile menu `useState` + scroll handlers, above - the fold), `AboutGallery` (carousel `useState`/`useEffect`, below fold), - `Counter` (×7 in About stats, `IntersectionObserver` count-up, below fold), - `OfferWidget` (private/company tab `useState`, below fold). - - Everything else has zero hooks/handlers — purely static markup: Hero shell, - `ServicesGrid`, `Testimonials`, `BookingWidget` (just a styled link), - `OfferPanel`, `OpeningHours`, `OnlineBookingCta` (decorative mockups only), - `Footer`, `LogoMark`, `GreenOnLogo`, `social-icons`. - - Two options, pick one: - - **Full fix**: rewrite the static components as plain `.astro` markup (no JS - shipped at all), keep only the 4 interactive pieces above as small islands - (`Navbar` → `client:load`, the other 3 → `client:visible`). Biggest win, - touches ~10 files, needs a visual QA pass + the existing Playwright/e2e specs. - - **Smaller fix**: keep everything as React, but stop mounting it all as one - `client:load` blob — split `Main` into per-section mounts in `index.astro` and - hydrate each with the directive matching its need (interactive ones as above, - static ones as `client:visible` just to defer their bytes past LCP). Lower - effort/risk, smaller win. - -## Cleanup (not a performance issue — verified negligible impact, see below) - -- [ ] **Decouple `cookies` module from `privacy-policy` module.** - `shared/policy/cookies/presentation/main.tsx:6-8` statically imports - `PrivacyPolicyContent`/`PrivacyPolicyIntro` at module scope, so that code ships - in the Cookies island's bundle even though it only renders when - `consent.view === 'policy'`. Checked the actual impact: the resulting chunk is - 4.18 KiB, ~4% of total page JS, loaded in parallel with everything else — it - does not meaningfully affect LCP or clear Lighthouse's "unused JS" threshold. - Fix for module-boundary hygiene, not speed: replace the static import with a - dynamic `import()` gated on opening the policy view (e.g. `React.lazy` + - `Suspense`, or an `import()` inside `openPolicyFromBanner`/`openSettings`). +# TODO + +Follow-ups from the PageSpeed Insights performance audit (mobile, 2026-07-25). +Image sizing, font preload mismatch, and the oversized footer SVG are already fixed. +Remaining items below. + +## Performance + +- [x] **Stop hydrating the whole page as one `client:load` island.** + Fixed: static sections (`Hero`, `Testimonials`, `ServicesGrid`, `BookingWidget`, + `OpeningHours`, `OnlineBookingCta`, `Footer`) now render as plain SSR markup with + zero client JS. `About`/`Offer`/`OfferPanel` were rewritten as `.astro` components + so only the genuinely interactive pieces ship JS: `Navbar` (`client:load`, + above the fold) and `AboutGallery`/`Counter`/`OfferWidget` (`client:visible`, + below the fold). See `modules/home/presentation/home.astro`. + `pages/index.astro` mounts `Main` (`modules/home/presentation/main.tsx`) — which + renders Navbar + Hero + Testimonials + About + Offer + OnlineBookingCta + Footer — + as a single eager React tree. This is the main driver of LCP being 4.2s (poor): + it forces ~83KB of React/ReactDOM/jsx-runtime/Astro-renderer JS onto the critical + path competing with the hero image for bandwidth, and produces the "Reduce unused + JavaScript" finding (42% of the 56KB Astro client runtime chunk unused). + + Audited which components actually need client JS (grepped for hooks/handlers): + - Genuinely interactive: `Navbar` (mobile menu `useState` + scroll handlers, above + the fold), `AboutGallery` (carousel `useState`/`useEffect`, below fold), + `Counter` (×7 in About stats, `IntersectionObserver` count-up, below fold), + `OfferWidget` (private/company tab `useState`, below fold). + - Everything else has zero hooks/handlers — purely static markup: Hero shell, + `ServicesGrid`, `Testimonials`, `BookingWidget` (just a styled link), + `OfferPanel`, `OpeningHours`, `OnlineBookingCta` (decorative mockups only), + `Footer`, `LogoMark`, `GreenOnLogo`, `social-icons`. + + Two options, pick one: + - **Full fix**: rewrite the static components as plain `.astro` markup (no JS + shipped at all), keep only the 4 interactive pieces above as small islands + (`Navbar` → `client:load`, the other 3 → `client:visible`). Biggest win, + touches ~10 files, needs a visual QA pass + the existing Playwright/e2e specs. + - **Smaller fix**: keep everything as React, but stop mounting it all as one + `client:load` blob — split `Main` into per-section mounts in `index.astro` and + hydrate each with the directive matching its need (interactive ones as above, + static ones as `client:visible` just to defer their bytes past LCP). Lower + effort/risk, smaller win. + +## Cleanup (not a performance issue — verified negligible impact, see below) + +- [ ] **Decouple `cookies` module from `privacy-policy` module.** + `shared/policy/cookies/presentation/main.tsx:6-8` statically imports + `PrivacyPolicyContent`/`PrivacyPolicyIntro` at module scope, so that code ships + in the Cookies island's bundle even though it only renders when + `consent.view === 'policy'`. Checked the actual impact: the resulting chunk is + 4.18 KiB, ~4% of total page JS, loaded in parallel with everything else — it + does not meaningfully affect LCP or clear Lighthouse's "unused JS" threshold. + Fix for module-boundary hygiene, not speed: replace the static import with a + dynamic `import()` gated on opening the policy view (e.g. `React.lazy` + + `Suspense`, or an `import()` inside `openPolicyFromBanner`/`openSettings`). diff --git a/apps/kdebek/src/modules/home/index.ts b/apps/kdebek/src/modules/home/index.ts index 18c230c..4ea403b 100644 --- a/apps/kdebek/src/modules/home/index.ts +++ b/apps/kdebek/src/modules/home/index.ts @@ -1,4 +1,3 @@ -export { Main } from './presentation/main'; -export { copy } from './presentation/copy'; -export { resolveHomeImages } from './integration/resolve-home-images'; -export type { HomeImages, ResolvedImage } from './domain/models'; +export { copy } from './presentation/copy'; +export { resolveHomeImages } from './integration/resolve-home-images'; +export type { HomeImages, ResolvedImage } from './domain/models'; diff --git a/apps/kdebek/src/modules/home/presentation/about.astro b/apps/kdebek/src/modules/home/presentation/about.astro new file mode 100644 index 0000000..c185bb6 --- /dev/null +++ b/apps/kdebek/src/modules/home/presentation/about.astro @@ -0,0 +1,147 @@ +--- +import { + Award, + BookOpen, + FileText, + Heart, + Quote, + Star, + Target, + Users, + type LucideIcon, +} from 'lucide-react'; + +import type { ResolvedImage } from '../domain/models'; + +import { AboutGallery } from './about-gallery'; +import { Counter } from './counter'; +import { copy } from './copy'; + +type Props = { + galleryImages: ResolvedImage[]; +}; + +const { galleryImages }: Props = Astro.props; + +const credentialIcons: Record = { + experience: FileText, + specializations: Target, + approach: Heart, +}; + +const statIcons: Record = { + clients: Users, + visits: FileText, + 'google-rating': Star, + 'booksy-rating': Star, + years: Award, + courses: BookOpen, +}; +--- + +
+
+
+
+

+ {copy.about.eyebrow} +

+

+ {copy.about.headingLine1} +
+ {copy.about.headingLine2} +

+
+ { + copy.about.introParagraphs.map((paragraph) => ( +

+ {paragraph} +

+ )) + } +
+ + +
    + { + copy.about.credentials.map(({ id, title, description }) => { + const Icon = credentialIcons[id]; + + return ( +
  • + +
    +

    {title}

    +

    + {description} +

    +
    +
  • + ); + }) + } +
+ +
+
+
+ + +
+
+ +
+
+
+
+
+
+ {copy.about.statsDescription} +
+
+ { + copy.about.stats.map((stat) => { + const { id, value, suffix, label } = stat; + const decimals = 'decimals' in stat ? stat.decimals : undefined; + const Icon = statIcons[id]; + + return ( +
+
+
+
+ +
+ +
+ ); + }) + } +
+
+
diff --git a/apps/kdebek/src/modules/home/presentation/about.tsx b/apps/kdebek/src/modules/home/presentation/about.tsx deleted file mode 100644 index 753bc81..0000000 --- a/apps/kdebek/src/modules/home/presentation/about.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import { - Award, - BookOpen, - FileText, - Heart, - Quote, - Star, - Target, - Users, - type LucideIcon, -} from 'lucide-react'; - -import type { ResolvedImage } from '../domain/models'; - -import { AboutGallery } from './about-gallery'; -import { Counter } from './counter'; -import { copy } from './copy'; - -const credentialIcons: Record = { - experience: FileText, - specializations: Target, - approach: Heart, -}; - -const statIcons: Record = { - clients: Users, - visits: FileText, - 'google-rating': Star, - 'booksy-rating': Star, - years: Award, - courses: BookOpen, -}; - -type AboutProps = { - galleryImages: ResolvedImage[]; -}; - -export const About = ({ galleryImages }: AboutProps) => ( -
-
-
-
-

- {copy.about.eyebrow} -

-

- {copy.about.headingLine1} -
- {copy.about.headingLine2} -

-
- {copy.about.introParagraphs.map((paragraph) => ( -

- {paragraph} -

- ))} -
- - -
    - {copy.about.credentials.map(({ id, title, description }) => { - const Icon = credentialIcons[id]; - - return ( -
  • - -
    -

    {title}

    -

    - {description} -

    -
    -
  • - ); - })} -
- -
-
-
- - -
-
- -
-
-
-
-
-
- {copy.about.statsDescription} -
-
- {copy.about.stats.map((stat) => { - const { id, value, suffix, label } = stat; - const decimals = 'decimals' in stat ? stat.decimals : undefined; - const Icon = statIcons[id]; - - return ( -
-
-
-
- -
- -
- ); - })} -
-
-
-); diff --git a/apps/kdebek/src/modules/home/presentation/home.astro b/apps/kdebek/src/modules/home/presentation/home.astro new file mode 100644 index 0000000..25050eb --- /dev/null +++ b/apps/kdebek/src/modules/home/presentation/home.astro @@ -0,0 +1,27 @@ +--- +import type { HomeImages } from '../domain/models'; + +import About from './about.astro'; +import { Footer } from './footer'; +import { Hero } from './hero'; +import { Navbar } from './navbar'; +import Offer from './offer.astro'; +import { OnlineBookingCta } from './online-booking-cta'; +import { Testimonials } from './testimonials'; + +type Props = { + images: HomeImages; +}; + +const { images }: Props = Astro.props; +--- + + +
+ + + + + +
+