From c52cdfca301c63c286fba3a1663e0478fa8e9408 Mon Sep 17 00:00:00 2001 From: Ahmet Abdullah Gultekin Date: Fri, 12 Jun 2026 10:17:59 +0000 Subject: [PATCH] perf(auth): static gradient + shared one-shot orbs across all auth pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 7 standalone auth shells (Register, SecondaryAuthFlow, VerifyEmail, ResetPassword, Onboarding, AcceptInvite, ForgotPassword) each carried a byte-identical copy of the login-page jank anti-pattern that PR #217 already removed from LoginPage: - an infinite `gradientShift` keyframe animation over `backgroundSize: 400% 400%`, which animates `background-position` and so forces a full-viewport REPAINT every frame (main-thread when HW-accel is off); and - decorative `FloatingShape` orbs that ran an INFINITE framer-motion loop with a per-frame `backdrop-filter: blur(10px)`. Fix (presentational/perf only, behaviour-preserving): - Replace every inline animated-gradient `sx` block with the shared `loginShellBackgroundSx()` (static brand gradient, same stops) from loginBackground.ts. The `@keyframes gradientShift` are deleted. - Extract ONE shared decorative-orb component, `features/auth/components/FloatingShape.tsx` — the cheap ONE-SHOT fade/scale-in with a soft radial fill and no animated backdrop-blur (the target version LoginPage already shipped). LoginPage and all 7 pages now import it and their local inline `FloatingShape` definitions are removed (8 duplicate copies -> 1). - Gate the orbs behind `!usePrefersReducedMotion()` on every page that renders them, matching LoginPage. Each page keeps its exact colours, layout, glass-card `blur(20px)`, and orb positions/sizes/delays. Orb counts are preserved (5 on most; VerifyEmail keeps its 4; SecondaryAuthFlow renders none and only had an unused FloatingShape def + the animated gradient, both removed). Net -196 LOC across the 8 files plus the one shared component. Verified: tsc --noEmit clean, eslint clean on all changed files, vitest src/features/auth + src/pages 395/395 green, vite build OK. --- .../auth/components/FloatingShape.tsx | 46 ++++++++++++++ src/features/auth/components/LoginPage.tsx | 35 +---------- src/features/auth/components/RegisterPage.tsx | 62 +++++-------------- .../auth/components/SecondaryAuthFlow.tsx | 14 +---- src/pages/AcceptInvitePage.tsx | 51 +++++---------- src/pages/ForgotPasswordPage.tsx | 60 +++++------------- src/pages/OnboardingPage.tsx | 51 +++++---------- src/pages/ResetPasswordPage.tsx | 60 +++++------------- src/pages/VerifyEmailPage.tsx | 49 +++++---------- 9 files changed, 139 insertions(+), 289 deletions(-) create mode 100644 src/features/auth/components/FloatingShape.tsx diff --git a/src/features/auth/components/FloatingShape.tsx b/src/features/auth/components/FloatingShape.tsx new file mode 100644 index 00000000..5682dd6f --- /dev/null +++ b/src/features/auth/components/FloatingShape.tsx @@ -0,0 +1,46 @@ +import { motion } from 'framer-motion' + +/** + * FloatingShape — the shared decorative background orb for every auth shell + * (dashboard login + register + MFA + the standalone auth pages). + * + * ONE-SHOT fade/scale-in only (no `repeat: Infinity`) with a soft radial fill + * instead of an animated `backdrop-filter: blur`. The previous orb ran an + * INFINITE framer-motion loop AND re-blurred its backdrop every frame, which — + * layered over the (now removed) animated shell gradient — was a continuous + * main-thread compositing cost (worst with hardware-acceleration off) that + * contributed to the auth-page jank. After the entrance transition this orb is + * fully static, so it adds zero per-frame work while staying as brand + * decoration. See loginBackground.ts for the matching static-gradient fix. + * + * Callers gate the orbs behind `!usePrefersReducedMotion()` so users who ask + * for reduced motion get no decorative chrome at all. + */ +const FloatingShape = ({ delay, size, left, top }: { + delay: number + size: number + left: string + top: string +}) => ( + +) + +export default FloatingShape diff --git a/src/features/auth/components/LoginPage.tsx b/src/features/auth/components/LoginPage.tsx index 7cba4573..9562d431 100644 --- a/src/features/auth/components/LoginPage.tsx +++ b/src/features/auth/components/LoginPage.tsx @@ -55,6 +55,7 @@ import { hasPasswordLayer1, selectPuzzleConfig, type LoginConfig } from '@domain import StepProgress from '../../../verify-app/StepProgress' import { usePrefersReducedMotion } from '@hooks/usePrefersReducedMotion' import { loginShellBackgroundSx, glassCardBackdropFilter } from './loginBackground' +import FloatingShape from './FloatingShape' /** * Login form validation schema @@ -108,40 +109,6 @@ const logoVariants: Variants = { }, } -// Decorative background orb. ONE-SHOT fade/scale-in only (no `repeat: Infinity`) -// and a soft radial fill instead of an animated `backdrop-filter: blur` — the -// continuous orb animation + per-frame backdrop re-blur was a main-thread compositing -// cost (worst with hardware-acceleration off) that contributed to the login jank. -// After the entrance it is fully static, so it adds zero per-frame work — the orbs -// stay as brand decoration without the cost. See loginBackground.ts for the matching -// static-gradient fix. -const FloatingShape = ({ delay, size, left, top }: { - delay: number - size: number - left: string - top: string -}) => ( - -) - /** * Login Page Component * Beautiful animated login with glassmorphism design diff --git a/src/features/auth/components/RegisterPage.tsx b/src/features/auth/components/RegisterPage.tsx index c5a8b446..9d7d9c1e 100644 --- a/src/features/auth/components/RegisterPage.tsx +++ b/src/features/auth/components/RegisterPage.tsx @@ -39,6 +39,9 @@ import { getService } from '@core/di/container' import { TYPES } from '@core/di/types' import type { IHttpClient } from '@domain/interfaces/IHttpClient' import { isLikelyValidEmail } from '@domain/validators/emailValidator' +import { usePrefersReducedMotion } from '@hooks/usePrefersReducedMotion' +import { loginShellBackgroundSx } from './loginBackground' +import FloatingShape from './FloatingShape' /** * Register form validation schema @@ -117,39 +120,6 @@ const logoVariants: Variants = { }, } -// Floating shapes for background -const FloatingShape = ({ delay, size, left, top }: { - delay: number - size: number - left: string - top: string -}) => ( - -) - /** * Register Page Component * Beautiful animated registration with glassmorphism design @@ -157,6 +127,7 @@ const FloatingShape = ({ delay, size, left, top }: { export default function RegisterPage() { const { t } = useTranslation() const navigate = useNavigate() + const prefersReducedMotion = usePrefersReducedMotion() const [showPassword, setShowPassword] = useState(false) const [showConfirmPassword, setShowConfirmPassword] = useState(false) const [loading, setLoading] = useState(false) @@ -226,23 +197,20 @@ export default function RegisterPage() { justifyContent: 'center', position: 'relative', overflow: 'hidden', - background: 'linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f64f59 100%)', - backgroundSize: '400% 400%', - animation: 'gradientShift 15s ease infinite', - '@keyframes gradientShift': { - '0%': { backgroundPosition: '0% 50%' }, - '50%': { backgroundPosition: '100% 50%' }, - '100%': { backgroundPosition: '0% 50%' }, - }, py: 4, + ...loginShellBackgroundSx(), }} > - {/* Animated background shapes */} - - - - - + {/* Decorative background shapes — skipped for reduced-motion users. */} + {!prefersReducedMotion && ( + <> + + + + + + + )} ( - -) - export default function AcceptInvitePage() { const navigate = useNavigate() const { t } = useTranslation() + const prefersReducedMotion = usePrefersReducedMotion() const [searchParams] = useSearchParams() const token = searchParams.get('token') || '' const [loading, setLoading] = useState(false) @@ -203,21 +184,19 @@ export default function AcceptInvitePage() { justifyContent: 'center', position: 'relative', overflow: 'hidden', - background: 'linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f64f59 100%)', - backgroundSize: '400% 400%', - animation: 'gradientShift 15s ease infinite', - '@keyframes gradientShift': { - '0%': { backgroundPosition: '0% 50%' }, - '50%': { backgroundPosition: '100% 50%' }, - '100%': { backgroundPosition: '0% 50%' }, - }, + ...loginShellBackgroundSx(), }} > - - - - - + {/* Decorative background shapes — skipped for reduced-motion users. */} + {!prefersReducedMotion && ( + <> + + + + + + + )} ( - -) - export default function ForgotPasswordPage() { const navigate = useNavigate() const { t } = useTranslation() + const prefersReducedMotion = usePrefersReducedMotion() const [loading, setLoading] = useState(false) const [error, setError] = useState(null) @@ -148,21 +120,19 @@ export default function ForgotPasswordPage() { justifyContent: 'center', position: 'relative', overflow: 'hidden', - background: 'linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f64f59 100%)', - backgroundSize: '400% 400%', - animation: 'gradientShift 15s ease infinite', - '@keyframes gradientShift': { - '0%': { backgroundPosition: '0% 50%' }, - '50%': { backgroundPosition: '100% 50%' }, - '100%': { backgroundPosition: '0% 50%' }, - }, + ...loginShellBackgroundSx(), }} > - - - - - + {/* Decorative background shapes — skipped for reduced-motion users. */} + {!prefersReducedMotion && ( + <> + + + + + + + )} ( - -) - export default function OnboardingPage() { const navigate = useNavigate() const { t } = useTranslation() + const prefersReducedMotion = usePrefersReducedMotion() const [loading, setLoading] = useState(false) const [error, setError] = useState(null) const [submittedEmail, setSubmittedEmail] = useState(null) @@ -201,21 +182,19 @@ export default function OnboardingPage() { position: 'relative', overflow: 'hidden', py: 4, - background: 'linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f64f59 100%)', - backgroundSize: '400% 400%', - animation: 'gradientShift 15s ease infinite', - '@keyframes gradientShift': { - '0%': { backgroundPosition: '0% 50%' }, - '50%': { backgroundPosition: '100% 50%' }, - '100%': { backgroundPosition: '0% 50%' }, - }, + ...loginShellBackgroundSx(), }} > - - - - - + {/* Decorative background shapes — skipped for reduced-motion users. */} + {!prefersReducedMotion && ( + <> + + + + + + + )} ( - -) - export default function ResetPasswordPage() { const navigate = useNavigate() const { t } = useTranslation() + const prefersReducedMotion = usePrefersReducedMotion() const [searchParams] = useSearchParams() const email = searchParams.get('email') || '' const [loading, setLoading] = useState(false) @@ -210,21 +182,19 @@ export default function ResetPasswordPage() { justifyContent: 'center', position: 'relative', overflow: 'hidden', - background: 'linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f64f59 100%)', - backgroundSize: '400% 400%', - animation: 'gradientShift 15s ease infinite', - '@keyframes gradientShift': { - '0%': { backgroundPosition: '0% 50%' }, - '50%': { backgroundPosition: '100% 50%' }, - '100%': { backgroundPosition: '0% 50%' }, - }, + ...loginShellBackgroundSx(), }} > - - - - - + {/* Decorative background shapes — skipped for reduced-motion users. */} + {!prefersReducedMotion && ( + <> + + + + + + + )} ( - -) - type VerifyState = 'verifying' | 'success' | 'error' | 'noToken' export default function VerifyEmailPage() { const navigate = useNavigate() const { t } = useTranslation() + const prefersReducedMotion = usePrefersReducedMotion() const [searchParams] = useSearchParams() const token = searchParams.get('token') || '' const [state, setState] = useState(token ? 'verifying' : 'noToken') @@ -138,20 +119,18 @@ export default function VerifyEmailPage() { justifyContent: 'center', position: 'relative', overflow: 'hidden', - background: 'linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f64f59 100%)', - backgroundSize: '400% 400%', - animation: 'gradientShift 15s ease infinite', - '@keyframes gradientShift': { - '0%': { backgroundPosition: '0% 50%' }, - '50%': { backgroundPosition: '100% 50%' }, - '100%': { backgroundPosition: '0% 50%' }, - }, + ...loginShellBackgroundSx(), }} > - - - - + {/* Decorative background shapes — skipped for reduced-motion users. */} + {!prefersReducedMotion && ( + <> + + + + + + )}