=== Performance Gate Report === Generated: 2026-09-08 Ticket: #3273 — Forgot Password Feature Implementation Gate: W14b — Performance (CSS / Render-Hydration / Animation) Audited files: app/(auth)/forgot-password/page.tsx app/(auth)/forgot-password/verify/page.tsx app/(auth)/forgot-password/reset/page.tsx components/auth/ForgotPasswordForm.tsx components/auth/VerifyOtpForm.tsx components/auth/ResetPasswordForm.tsx components/auth/SignInForm.tsx Skills invoked: /nextjs-tailwind-css-performance-audit, /nextjs-tailwind-performance-audit, /nextjs-tailwind-animation-performance-audit (skills unavailable in this environment; manual static audit performed against the real source files below) Bundle baseline (from bundle-analyze-W14a.txt, produced by bundle-analyze-gate): /forgot-password 1.26 kB route, 109 kB First Load JS /forgot-password/verify 1.97 kB route, 110 kB First Load JS /forgot-password/reset 2.37 kB route, 101 kB First Load JS /login (SignInForm) 3.36 kB route, 108 kB First Load JS Build status: PASS (Next.js 14.2.35) ════════════════════════════════════════════════════════ AUDIT 1: CSS PERFORMANCE ════════════════════════════════════════════════════════ MEASURED: 1. All three auth form components use `fixed inset-0 z-[100] flex overflow-hidden bg-surface` on their outer div. The `z-[100]` is an arbitrary Tailwind value that compiles to a unique atomic class not shared elsewhere. Each auth route creates a full-viewport fixed stacking context. This is intentional (full-page form overlay) and correct for the design, but produces three near-identical CSS rule sets (one per component). No layout thrash risk — `fixed` elements are out of normal flow and do not affect sibling layout. 2. VerifyOtpForm applies two long arbitrary shadow values to each of 6 OTP inputs: shadow-[inset_0_0_0_1px_theme(colors.border.field)] (default state) shadow-[inset_0_0_0_2px_theme(colors.accent.DEFAULT)] (focus state) shadow-[inset_0_0_0_1px_theme(colors.status.danger)] (error state) shadow-[inset_0_0_0_2px_theme(colors.status.danger)] (error+focus) These four arbitrary shadow classes are unique to this component and will each generate a distinct CSS rule. Box-shadow changes trigger GPU-composited repaint only (no layout); acceptable for 6 small inputs. Total CSS bloat: 4 unique atomic rules, ~400 bytes. 3. ResetPasswordForm uses `bg-[rgb(230,230,230)]` as the "empty" bar colour on the strength meter — a hardcoded RGB value not mapped to any design token in tailwind.config.ts. Not a runtime performance concern, but this colour will not respond to theme changes. 4. BrandPanel (components/auth/BrandPanel.tsx) uses `Array.from({ length: 7 })` to render decorative dots. Since BrandPanel has no "use client" directive and is purely static markup, the array is evaluated server-side at render time and emitted as 7 static
elements. Zero client-side cost. 5. .field-input utility class (globals.css) is applied to the username input in SignInForm. It is a Tailwind @layer components rule that composes ~8 atomic utilities. No performance concern — it's already in the shared CSS chunk. INFERRED (not measured): 6. BrandPanel is imported by all three Client Component forms. Because the (auth) layout (app/(auth)/layout.tsx) renders only `<>{children}` with no shared markup, BrandPanel is bundled into all three Client Component trees. Webpack's chunk-splitting may deduplicate it into the shared fd9d1056 chunk (168.79 kB "Shared UI components" in bundle report) but this cannot be confirmed without per-component bundle analysis. If BrandPanel is NOT deduplicated, its ~50-line SVG markup is bundled 3× client-side. Moving BrandPanel to the auth layout and extracting the form-column as the only Client Component would guarantee deduplication and reduce First Load JS. (Inferred from code structure — not measured.) ════════════════════════════════════════════════════════ AUDIT 2: RENDER / HYDRATION PERFORMANCE ════════════════════════════════════════════════════════ MEASURED: 1. VerifyOtpForm mounts a 30-second countdown timer via useEffect + setTimeout. Each tick calls `setCountdown((c) => c - 1)`, which schedules a state update that causes the entire VerifyOtpForm component to re-render. Over the full countdown, this is approximately 30 re-renders triggered by the timer alone (one per second). Each re-render recomputes: - `otp = digits.join("")` (6-element join) - `otpError`, `sessionError` (two ternary reads) - clsx calls on 6 OTP input elements (12 class-string evaluations) BrandPanel is stable (no props, no hooks) so React's bailout prevents re-rendering it. The cost is real but minor — all operations are O(1) or O(6). No layout thrash. RECOMMENDATION: Extract the countdown display + resend button into a `` sub-component with its own state. This confines the 30 timer re-renders to that small sub-tree, shielding the OTP inputs from being re-evaluated each tick. 2. All three page shells (page.tsx files) are Server Components. They read cookies via next/headers and either redirect() or return the Client Component. Hydration boundary is at the Client Component level — server renders the full HTML including BrandPanel, client hydrates only the interactive form column. This is architecturally correct. 3. useFormState (react-dom) is used in all three form components. This is the correct API for Next.js 14.2 (useActionState was added in React 19 / Next.js 15). No deprecation concern for this version. 4. SignInForm is a lean Client Component: one useFormState hook, two inputs, one PasswordField, one SubmitButton. No timers, no computed state, no memoization needed. Re-renders occur only on form submission and server action response — correct behaviour. 5. ResetPasswordForm uses useMemo for `rulesMet` (RULES.map over newPassword). The 5 regex tests are cheap, but memoization is still appropriate as it prevents redundant regex evaluation on unrelated state changes (e.g. confirmPassword changes). Derived values (strengthBars, strengthLabel, canSubmit) are inline computations on top of rulesMet — no additional memoization needed given their simplicity. 6. SubmitButton (components/ui/SubmitButton.tsx) uses useFormStatus, which is scoped to the nearest parent form. It only re-renders when the form's pending state changes. Correct pattern — no performance concern. INFERRED (not measured): 7. If React's chunk splitting does NOT deduplicate BrandPanel across the three Client Component bundles (see CSS finding #6), each route hydrates BrandPanel separately, increasing hydration work. Playwright/profiler data is unavailable in this environment to confirm or refute. (Inferred from bundle structure — not measured.) ════════════════════════════════════════════════════════ AUDIT 3: ANIMATION PERFORMANCE ════════════════════════════════════════════════════════ MEASURED: 1. No Tailwind animation classes (animate-*, transition-*, duration-*, ease-*) found in any of the 7 audited files. 2. No Framer Motion, GSAP, or any JavaScript-driven animation library imports found. 3. No CSS @keyframes, will-change, or transform-based animation properties found. 4. The strength meter bars (ResetPasswordForm) switch between bg-status-success and bg-[rgb(230,230,230)] via clsx on each keystroke. These are instant class-swaps (a repaint, not an animation). No transition is applied. Acceptable — the switch is perceptually instant and imposes no compositor cost. 5. OTP digit boxes (VerifyOtpForm) switch between two box-shadow values on focus/error. No CSS transition is applied to the shadow change — it switches instantly. No animation performance concern. 6. The resend section in VerifyOtpForm conditionally renders either a countdown or a