fix(marketing): reduce CLS on mobile landing page#1053
Conversation
Remove Framer Motion entrance animations that caused layout shifts: - Add font-display: swap to prevent FOIT/FOUT - Remove y-axis animations from HeroSection, VideoSection, TrustedBySection, FeaturesSection, WallOfLoveSection, FAQSection, and CTASection - Keep user-triggered animations (FAQ accordion, AppMockup interactions) This should reduce CLS from 0.37 to below 0.25 threshold.
|
Caution Review failedThe pull request is closed. 📝 WalkthroughWalkthroughThis PR removes Framer Motion usage across multiple marketing components (replacing Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
🚀 Preview Deployment🔗 Preview Links
Preview updates automatically with new commits |
- Lazy load ShaderAnimation (THREE.js) with dynamic import - Lazy load MeshGradient (stripe-gradient) with dynamic import - Add priority and sizes to video thumbnail for LCP optimization - Disable SSR for WebGL components to reduce initial bundle This should significantly improve LCP (was 19.6s) by deferring heavy JavaScript execution until after initial render.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In
`@apps/marketing/src/app/components/HeroSection/components/ProductDemo/ProductDemo.tsx`:
- Line 4: The dynamic import for MeshGradient currently uses { ssr: false }
without a loading fallback; update the dynamic(...) call that creates
MeshGradient (the import("@superset/ui/mesh-gradient").then(mod =>
mod.MeshGradient) expression) to include a loading option that returns a simple
placeholder element (e.g., a div or skeleton with stable width/height and
appropriate classes such as "w-full h-full bg-slate-900") so the component shows
a non-empty fallback while the chunk loads.
In `@apps/marketing/src/app/components/HeroSection/HeroSection.tsx`:
- Around line 4-18: The dynamic ShaderAnimation import lacks a loading
placeholder which causes CLS when it hydrates; update the dynamic call for
ShaderAnimation (the dynamic(...) that returns mod.ShaderAnimation) to include a
loading option that returns a placeholder element sized to the final animation
(e.g., a div with an appropriate minHeight or fixed height) and keep ssr: false
so the layout space is reserved during client-side hydration.
|
|
||
| import { MeshGradient } from "@superset/ui/mesh-gradient"; | ||
| import { motion } from "framer-motion"; | ||
| import dynamic from "next/dynamic"; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n apps/marketing/src/app/components/HeroSection/components/ProductDemo/ProductDemo.tsx | head -25Repository: superset-sh/superset
Length of output: 1025
🌐 Web query:
Next.js 15 next/dynamic ssr: false best practices client-only components
💡 Result:
Best practices for client‑only components in Next.js 15 using next/dynamic({ ssr: false })
-
Only use
ssr: falseinside a Client Component- In the App Router,
ssr: falseis not supported in Server Components—move thedynamic(..., { ssr: false })call into a file with'use client'. This is explicitly required by the Next.js lazy-loading docs. [1]
- In the App Router,
-
Create a small client “wrapper” and keep the rest as Server Components
- Pattern: Server Component page/layout → imports a small Client Component wrapper → wrapper dynamically imports the heavy/browser-only component with
ssr:false. - This preserves the benefits of Server Components while isolating true browser-only code. [1]
- Pattern: Server Component page/layout → imports a small Client Component wrapper → wrapper dynamically imports the heavy/browser-only component with
-
Provide a
loadingfallback (and reserve layout space)- Since the component won’t be prerendered, users will see the fallback until the JS chunk loads. Use
loading: () => <Skeleton />and ensure the skeleton has stable dimensions to reduce layout shift. (Supported directly bynext/dynamicpatterns in Next.js docs.) [1]
- Since the component won’t be prerendered, users will see the fallback until the JS chunk loads. Use
-
Use
ssr: falsefor truly browser-only dependencies- Typical cases: components that touch
window/document, depend on browser-only libraries, or trigger hydration mismatches if prerendered. - Next.js explicitly recommends disabling prerendering for specific components as a hydration-mismatch fix. [2]
- Typical cases: components that touch
-
Avoid
ssr: falsefor above-the-fold / SEO-critical content- Because it removes server-rendered HTML for that subtree, it can hurt perceived performance and crawlability. Prefer normal Client Components (which can still be prerendered) when possible. [1]
-
Know the current limitation around code splitting when importing from Server Components
- Next.js notes that when a Server Component dynamically imports a Client Component, automatic code splitting is currently not supported—another reason to keep the
dynamic()usage inside a Client wrapper. [1]
- Next.js notes that when a Server Component dynamically imports a Client Component, automatic code splitting is currently not supported—another reason to keep the
Recommended pattern (App Router)
// app/components/MapClient.tsx
'use client'
import dynamic from 'next/dynamic'
const Map = dynamic(() => import('./Map'), {
ssr: false,
loading: () => <div style={{ height: 320 }}>Loading map…</div>,
})
export default function MapClient() {
return <Map />
}// app/page.tsx (Server Component by default)
import MapClient from './components/MapClient'
export default function Page() {
return (
<main>
<MapClient />
</main>
)
}Sources: Next.js Lazy Loading guide (App Router) [1], Next.js hydration error guidance (disabling SSR on specific components) [2].
Citations:
[1] Next.js Docs – Lazy Loading (App Router) (nextjs.org)
[2] Next.js Docs – React hydration error message & fixes (nextjs.org)
Citations:
- 1: https://nextjs.org/docs/app/guides/lazy-loading?utm_source=openai
- 2: https://nextjs.org/docs/messages/react-hydration-error?utm_source=openai
Add a loading fallback to the MeshGradient dynamic import.
The ssr: false configuration is correctly placed within a Client Component ("use client"), which is the proper pattern for Next.js 15. However, Next.js best practices recommend providing a loading fallback when using { ssr: false } to improve UX while the component chunk loads:
const MeshGradient = dynamic(
() =>
import("@superset/ui/mesh-gradient").then((mod) => mod.MeshGradient),
{
ssr: false,
loading: () => <div className="w-full h-full bg-slate-900" /> // or a skeleton with stable dimensions
},
);This prevents a blank space during initial load and reduces layout shift.
🤖 Prompt for AI Agents
In
`@apps/marketing/src/app/components/HeroSection/components/ProductDemo/ProductDemo.tsx`
at line 4, The dynamic import for MeshGradient currently uses { ssr: false }
without a loading fallback; update the dynamic(...) call that creates
MeshGradient (the import("@superset/ui/mesh-gradient").then(mod =>
mod.MeshGradient) expression) to include a loading option that returns a simple
placeholder element (e.g., a div or skeleton with stable width/height and
appropriate classes such as "w-full h-full bg-slate-900") so the component shows
a non-empty fallback while the chunk loads.
| import dynamic from "next/dynamic"; | ||
| import { useState } from "react"; | ||
| import { FaGithub } from "react-icons/fa"; | ||
| import { ShaderAnimation } from "../../../components/ui/shader-animation"; | ||
| import { DownloadButton } from "../DownloadButton"; | ||
| import { WaitlistModal } from "../WaitlistModal"; | ||
| import { ProductDemo } from "./components/ProductDemo"; | ||
| import { TypewriterText } from "./components/TypewriterText"; | ||
|
|
||
| const ShaderAnimation = dynamic( | ||
| () => | ||
| import("../../../components/ui/shader-animation").then( | ||
| (mod) => mod.ShaderAnimation, | ||
| ), | ||
| { ssr: false }, | ||
| ); |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Next.js 15 next/dynamic ssr false client component CLS layout shift guidance
💡 Result:
Using next/dynamic with { ssr: false } in Next.js 15 (App Router)
ssr: falseonly works in Client Components. If you try to usedynamic(..., { ssr: false })in a Server Component, Next.js will error—move the dynamic import into a Client Component wrapper and render that wrapper from your Server Component. [1]- Don’t combine
suspense: truewithssr: false. Withsuspense: true, React will try to resolve Suspense on the server and Next.js documents thatssr: falseis effectively ignored / invalid in that configuration. [2]
Preventing CLS (layout shift) when the client-only component loads
When you render a client-only dynamic component, the initial HTML won’t contain it—so you must reserve space for whatever will appear after hydration:
- Provide a stable placeholder while loading (skeleton) that matches the final component’s size. With
next/dynamic, use theloadingoption to render a placeholder. [1] - Reserve space via CSS: wrap the dynamic component in a container with a predictable
min-height, or useaspect-ratiofor responsive boxes so the page layout doesn’t jump when the component appears. [3] - If the dynamic content is near the top of the viewport, CLS impact is typically worse—either reserve space carefully or consider placing late-loading UI lower on the page when possible. [3]
Minimal pattern (App Router)
// app/components/ClientOnlyWidget.tsx
'use client'
import dynamic from 'next/dynamic'
const Widget = dynamic(() => import('./Widget'), {
ssr: false,
loading: () => <div style={{ minHeight: 240 }} />, // reserve space to prevent CLS
})
export function ClientOnlyWidget() {
return <Widget />
}Key idea: the placeholder (or wrapper) should occupy roughly the same dimensions as the final widget to avoid CLS. [3]
Sources: Next.js Lazy Loading guide (updated Oct 15, 2025) [1], Next.js error note on suspense usage with ssr: false [2], web.dev CLS guidance on reserving space / placeholders / aspect-ratio [3].
🏁 Script executed:
# Find and examine the ShaderAnimation component
fd -e tsx -e ts | xargs rg -l "ShaderAnimation" | head -20Repository: superset-sh/superset
Length of output: 221
🏁 Script executed:
# Check the full HeroSection component for loading state/Suspense boundaries
cat -n apps/marketing/src/app/components/HeroSection/HeroSection.tsx | head -100Repository: superset-sh/superset
Length of output: 3340
Add loading placeholder to prevent CLS when ShaderAnimation hydrates.
ssr: false is appropriate for this client-only component, but it lacks a loading option with a placeholder that reserves space during client-side hydration. Since ShaderAnimation renders at the top of the hero section, the layout will shift when it loads. Provide a placeholder matching the component's final dimensions:
Suggested fix
const ShaderAnimation = dynamic(
() =>
import("../../../components/ui/shader-animation").then(
(mod) => mod.ShaderAnimation,
),
{
ssr: false,
loading: () => <div style={{ minHeight: 400 }} /> // adjust height to match ShaderAnimation's actual height
},
);🤖 Prompt for AI Agents
In `@apps/marketing/src/app/components/HeroSection/HeroSection.tsx` around lines 4
- 18, The dynamic ShaderAnimation import lacks a loading placeholder which
causes CLS when it hydrates; update the dynamic call for ShaderAnimation (the
dynamic(...) that returns mod.ShaderAnimation) to include a loading option that
returns a placeholder element sized to the final animation (e.g., a div with an
appropriate minHeight or fixed height) and keep ssr: false so the layout space
is reserved during client-side hydration.
Summary
font-display: swapto prevent font loading layout shiftsChanges
display: "swap"to IBM Plex Mono and Inter fontsTest plan
Summary by CodeRabbit
Refactor
Chores
✏️ Tip: You can customize this high-level summary in your review settings.