|
=== 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 <div>
|
|
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 `<ResendSection>`
|
|
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 <span> or
|
|
a <button> when countdown reaches 0. This is a conditional DOM node swap that may cause
|
|
a brief repaint at the transition point. It is not animated and occurs only once per
|
|
session. No concern.
|
|
|
|
════════════════════════════════════════════════════════
|
|
GATE VERDICT
|
|
════════════════════════════════════════════════════════
|
|
|
|
Gate Status: WARN
|
|
|
|
Reason for WARN:
|
|
- VerifyOtpForm's 30-second countdown causes ~30 full-component re-renders (once per
|
|
second), evaluated against 6 OTP inputs and their clsx logic on each tick. This is a
|
|
real but minor measured inefficiency. Extracting countdown+resend into its own sub-
|
|
component (ResendSection) would isolate the timer re-renders to a small leaf tree.
|
|
- The `bg-[rgb(230,230,230)]` hardcoded colour in ResetPasswordForm's strength meter
|
|
is not a design token and will not respond to theme changes (minor CSS maintainability
|
|
risk, not a runtime performance regression).
|
|
|
|
No animation performance issues found (none exist).
|
|
No blocking CSS or hydration performance issues found.
|
|
Bundle sizes are within expected range per bundle-analyze-gate's PASS report.
|