From 6d9eef56a1936e6e9ff189f5e4a31336d241619a Mon Sep 17 00:00:00 2001 From: Ignat Date: Mon, 25 May 2026 02:47:03 -0300 Subject: [PATCH 01/10] perf(landing/faq): hoist useReducedMotion, React.memo + useCallback on FaqItem (1-3/4) Kai lag-analysis 2026-05-25: 12 matchMedia subscriptions (one per FaqItem) collapsed to 1 in FaqSection. FaqItem wrapped in React.memo; handleToggle stabilised via useCallback + index passed as arg to avoid per-render arrow wrappers. backdrop-blur-sm removed from section root (Fix 2, FAQ part). --- .../landing/faq-section/FaqSection.tsx | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/src/widgets/landing/faq-section/FaqSection.tsx b/src/widgets/landing/faq-section/FaqSection.tsx index 6bb7f04e..a29a9626 100644 --- a/src/widgets/landing/faq-section/FaqSection.tsx +++ b/src/widgets/landing/faq-section/FaqSection.tsx @@ -6,7 +6,7 @@ 'use client' -import { useState } from 'react' +import { useState, useCallback, memo } from 'react' import { ChevronDownIcon } from '@/shared/ui/icons' import { useReducedMotion } from '@/shared/ui' @@ -17,14 +17,20 @@ import { AnimatePresence, motion } from '@/shared/ui/motion' import { FAQ_ITEMS, type FaqItem } from '../constants/faq' -function FaqItem({ question, answer, isOpen, onToggle, index }: FaqItem & { isOpen: boolean; onToggle: () => void; index: number }) { - const shouldReduceMotion = useReducedMotion() +const FaqItem = memo(function FaqItem({ + question, + answer, + isOpen, + onToggle, + index, + reducedMotion, +}: FaqItem & { isOpen: boolean; onToggle: (index: number) => void; index: number; reducedMotion: boolean }) { return (
) -} +}) export function FaqSection() { const [openIndex, setOpenIndex] = useState(0) + const reducedMotion = useReducedMotion() - const handleToggle = (index: number) => { - setOpenIndex(openIndex === index ? null : index) - } + const handleToggle = useCallback((index: number) => { + setOpenIndex((prev) => (prev === index ? null : index)) + }, []) return (
@@ -92,7 +99,8 @@ export function FaqSection() { question={item.question} answer={item.answer} isOpen={openIndex === index} - onToggle={() => handleToggle(index)} + onToggle={handleToggle} + reducedMotion={reducedMotion ?? false} /> ))}
From cc2dc46e71bf38e4d3c82965c6627712dbccdf48 Mon Sep 17 00:00:00 2001 From: Ignat Date: Mon, 25 May 2026 02:47:11 -0300 Subject: [PATCH 02/10] perf(landing): remove backdrop-blur-sm from ComparisonTable section root (2/4) Kai lag-analysis 2026-05-25: backdrop-blur-sm on section roots forces GPU compositing layers during scroll. Inner accordion card retains backdrop-blur-sm (visually load-bearing). Section root is now flat bg. --- src/widgets/landing/comparison/ComparisonTable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/widgets/landing/comparison/ComparisonTable.tsx b/src/widgets/landing/comparison/ComparisonTable.tsx index 7689166a..b5166169 100644 --- a/src/widgets/landing/comparison/ComparisonTable.tsx +++ b/src/widgets/landing/comparison/ComparisonTable.tsx @@ -139,7 +139,7 @@ function ValueCell({ value }: { value: ComparisonValue }) { export function ComparisonTable() { return ( -
+
{/* Section header - SEO: competitor comparison keywords */}
From 1ea0e9b90dde4668bd961013b0fdf462e1ea2129 Mon Sep 17 00:00:00 2001 From: Ignat Date: Mon, 25 May 2026 02:47:23 -0300 Subject: [PATCH 03/10] perf(landing/comparison): decouple ComparisonTable via children-prop pattern (4/4) Kai lag-analysis 2026-05-25: ComparisonTable has no hooks/state/handlers and does not need to be bundled inside BelowFoldSections client chunk. Moved to comparisonTable: ReactNode prop; LandingContent passes . Full RSC gain requires LandingContent server-shell split (follow-up for Kai). --- src/widgets/landing/ui/BelowFoldSections.tsx | 14 +++++++++++--- src/widgets/landing/ui/LandingContent.tsx | 7 ++++--- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/widgets/landing/ui/BelowFoldSections.tsx b/src/widgets/landing/ui/BelowFoldSections.tsx index 69487969..22ec1eae 100644 --- a/src/widgets/landing/ui/BelowFoldSections.tsx +++ b/src/widgets/landing/ui/BelowFoldSections.tsx @@ -12,23 +12,31 @@ * * All motion-dependent components are imported here to ensure * Framer Motion is bundled only in this chunk, not the initial load. + * + * comparisonTable is accepted as a ReactNode prop so ComparisonTable + * (which has no client-side needs) can be rendered by a server-side + * ancestor when the RSC boundary allows it. */ +import type { ReactNode } from 'react' import { HowItWorks } from '../how-it-works/HowItWorks' import { DemoSection } from '../demo-section/DemoSection' import { WhyVoidPay } from '../why-voidpay/WhyVoidPay' -import { ComparisonTable } from '../comparison/ComparisonTable' import { AudienceSection } from '../audience-section/AudienceSection' import { FaqSection } from '../faq-section' import { FooterCta } from '../footer-cta/FooterCta' -export function BelowFoldSections() { +interface BelowFoldSectionsProps { + comparisonTable: ReactNode +} + +export function BelowFoldSections({ comparisonTable }: BelowFoldSectionsProps) { return ( <> - + {comparisonTable} diff --git a/src/widgets/landing/ui/LandingContent.tsx b/src/widgets/landing/ui/LandingContent.tsx index e337ca84..70dc990f 100644 --- a/src/widgets/landing/ui/LandingContent.tsx +++ b/src/widgets/landing/ui/LandingContent.tsx @@ -21,11 +21,12 @@ * webpack from statically analyzing and bundling them. */ -import { useState, useEffect, type ComponentType } from 'react' +import { useState, useEffect, type ComponentType, type ReactNode } from 'react' import { useCreatorStore } from '@/entities/creator' import { HeroSection } from '../hero-section/HeroSection' import { SocialProofStrip } from '../social-proof' import { BelowFoldLoader } from './BelowFoldLoader' +import { ComparisonTable } from '../comparison/ComparisonTable' /** * SEO-friendly placeholder for below-fold content. @@ -52,7 +53,7 @@ function BelowFoldPlaceholder() { * LazyBelowFold - Loads below-fold sections ONLY when triggered */ function LazyBelowFold() { - const [Component, setComponent] = useState | null>(null) + const [Component, setComponent] = useState | null>(null) const [loadError, setLoadError] = useState(null) useEffect(() => { @@ -81,7 +82,7 @@ function LazyBelowFold() { } if (!Component) return - return + return } /> } export function LandingContent() { From c13000580982ae9e6d45e5998df98b5fb47faefa Mon Sep 17 00:00:00 2001 From: Ignat Date: Thu, 28 May 2026 16:06:11 -0300 Subject: [PATCH 04/10] perf(landing): add framer-motion and @web3icons/react to optimizePackageImports --- next.config.mjs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/next.config.mjs b/next.config.mjs index a5d88820..deff7ee6 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -28,6 +28,8 @@ const nextConfig = { 'wagmi', 'viem', '@rainbow-me/rainbowkit', + 'framer-motion', + '@web3icons/react', ], }, From 3fd08863f526c95fee7acc79ff29795e35b33381 Mon Sep 17 00:00:00 2001 From: Ignat Date: Thu, 28 May 2026 16:06:22 -0300 Subject: [PATCH 05/10] perf(landing): replace next/script JSON-LD with plain