|
Performance Gate Report — W14b
|
|
Ticket: #3269 (Forgot Password / Password Reset Flow)
|
|
Date: 2026-09-04
|
|
Audits: CSS Performance, General Render/Hydration Performance, Animation Performance
|
|
|
|
Feature files analyzed:
|
|
- middleware.ts
|
|
- app/(auth)/forgot-password/page.tsx
|
|
- app/(auth)/verify-otp/page.tsx
|
|
- app/(auth)/reset-password/page.tsx
|
|
- components/auth/ForgotPasswordForm.tsx
|
|
- components/auth/VerifyOtpForm.tsx
|
|
- components/auth/CompleteResetForm.tsx
|
|
|
|
Skill availability note:
|
|
Skills /nextjs-tailwind-css-performance-audit, /nextjs-tailwind-performance-audit,
|
|
/nextjs-tailwind-animation-performance-audit resolved to "Unknown skill" at runtime.
|
|
Audit was performed by direct file analysis against the actual source files.
|
|
|
|
Bundle-analyze gate status (already produced — not re-built here):
|
|
See docs/gate-evidence/bundle-analyze-3269.txt.
|
|
Build FAILED: lib/actions/forgot-password.ts exports a non-async constant
|
|
(RESET_FLOW_COOKIE) from a "use server" file — a Next.js constraint violation.
|
|
This is a supporting-file defect, not in the 7 feature files. Bundle size delta
|
|
cannot be confirmed from that report, but the 7 feature files contain no
|
|
heavy third-party imports beyond existing shared components (PasswordField,
|
|
SubmitButton, Toast, PhoneInput, BrandPanel) and react-dom/next/navigation hooks.
|
|
|
|
|
|
=======================================================================
|
|
AUDIT 1: CSS Performance
|
|
=======================================================================
|
|
|
|
MEASURED — Tailwind class analysis (direct source read):
|
|
|
|
Finding CSSPF-1 [css-performance] — No impact (informational)
|
|
Files: forgot-password/page.tsx:14, verify-otp/page.tsx:35, reset-password/page.tsx:35
|
|
Class string: "fixed inset-0 z-[100] flex overflow-hidden bg-surface"
|
|
Observation: Identical class string repeated across three auth pages (and
|
|
login/page.tsx — four total). Tailwind generates each utility class once in
|
|
the output stylesheet regardless of how many templates reference it — no CSS
|
|
file-size duplication. The (auth)/layout.tsx noted this in the quality audit
|
|
(CSSDRY-1) as a markup-DRY issue; it has zero CSS-output-size impact.
|
|
Verdict: No performance finding. Already flagged in W14 quality gate.
|
|
|
|
Finding CSSPF-2 [css-performance] — No impact (informational)
|
|
File: CompleteResetForm.tsx (lines 97-112), strength-bar divs
|
|
Observation: Four strength-bar divs use clsx() with conditional
|
|
bg-status-success / bg-border-subtle class toggling per keystroke.
|
|
Tailwind emits both classes unconditionally; the toggle is a className-string
|
|
swap (not a style mutation). No style recalculation beyond a single-element
|
|
class change per bar per keystroke. DOM depth is 4 sibling divs, cost negligible.
|
|
Verdict: No performance finding.
|
|
|
|
Finding CSSPF-3 [css-performance] — No impact (informational)
|
|
Files: Multiple
|
|
Arbitrary Tailwind values in use: z-[100], max-w-[360px], max-w-[600px],
|
|
max-w-[180px], max-w-[460px], max-w-[1080px], text-[26px], text-[13px],
|
|
h-[5px], gap-[3px], py-[18px], tracking-[0.5em]
|
|
Observation: Each arbitrary value generates a unique CSS class. Across the 7
|
|
files, 12 distinct arbitrary values are present. Estimated CSS-size overhead:
|
|
~300–400 bytes (12 rules × ~25–35 bytes each). Negligible at these quantities.
|
|
Verdict: No performance finding.
|
|
|
|
Areas checked and found clean (CSS):
|
|
- No @apply directives in any feature file (Tailwind plugin would purge them;
|
|
any @apply in globals.css is outside scope).
|
|
- No inline style= attributes except one intentional SVG stroke color override
|
|
in CompleteResetForm.tsx (rule-check SVG icons). Inline SVG stroke avoids a
|
|
separate SVG icon sprite or icon library import — acceptable trade-off.
|
|
- No bg-gradient or complex filter utilities that trigger expensive paint layers.
|
|
- No fixed-height containers that could cause content overflow or reflow loops.
|
|
- overflow-hidden on the auth page shell prevents scrollbar layout shift.
|
|
- overflow-y-auto on reset-password's right panel is correct (long form).
|
|
|
|
|
|
=======================================================================
|
|
AUDIT 2: General Render / Hydration Performance
|
|
=======================================================================
|
|
|
|
MEASURED — direct source analysis:
|
|
|
|
Finding PERFR-1 [render-performance] — No impact (informational)
|
|
File: components/auth/VerifyOtpForm.tsx (lines 41-45)
|
|
Pattern: setInterval(1-second tick) inside useEffect, updating secondsLeft state.
|
|
Observation: Each tick calls setSecondsLeft((s) => Math.max(0, s - 1)), triggering
|
|
one re-render per second. The re-rendered subtree is the resend-button text node
|
|
only ("Resend code in Ns"). The interval is cleared when secondsLeft <= 0 (the
|
|
early-return guard on line 42) and on unmount via the returned clearInterval.
|
|
Lifecycle is correct; re-render scope is minimal.
|
|
Verdict: No performance finding.
|
|
|
|
Finding PERFR-2 [render-performance] — No impact (informational)
|
|
File: components/auth/CompleteResetForm.tsx (lines 42-43)
|
|
Pattern: useMemo(() => RULES.map(r => r.test(newPassword)), [newPassword])
|
|
Observation: 5 lightweight regex tests (.length, /[A-Z]/, /[a-z]/, /[0-9]/,
|
|
/[^A-Za-z0-9]/) are memoized. The dependent computation (metCount, strengthBars,
|
|
strengthLabel, policyOk, matchOk, canSubmit) runs synchronously from the memoized
|
|
value. This is the correct pattern — no unnecessary recomputation.
|
|
RULES is module-level (not inside the component body), so its reference is stable
|
|
and won't defeat the memo.
|
|
Verdict: No performance finding.
|
|
|
|
Finding PERFR-3 [render-performance] — No impact (informational)
|
|
File: app/(auth)/verify-otp/page.tsx, app/(auth)/reset-password/page.tsx
|
|
Pattern: Server Component awaits getResetFlowState(flowId) — a DB call — at
|
|
render time, with redirect() on a falsy result.
|
|
Observation: These pages are gated behind the oms_reset_flow cookie, which only
|
|
exists for users mid-flow. Traffic volume is inherently low (forgot-password is
|
|
an infrequent path). The DB call selects by primary UUID — O(1) with an indexed
|
|
PK. No N+1 or unbounded query visible from the page files.
|
|
Verdict: No performance finding (measured within scope of these files).
|
|
|
|
Finding PERFR-4 [render-performance] — No impact (informational)
|
|
File: middleware.ts (lines 49–98)
|
|
Pattern: Edge middleware runs on every matched request; excludes /login,
|
|
/forgot-password, /verify-otp, /reset-password, /api/cron, /_next/*.
|
|
Observation: For unauthenticated reset-flow pages: the matcher excludes them,
|
|
so middleware is BYPASSED entirely for these routes (correct, by design per the
|
|
ticket comment on line 45). Middleware overhead for authenticated routes: awaits
|
|
verifyEdgeMarker (crypto — Ed25519 or HMAC, Edge-compatible); then a synchronous
|
|
permission lookup (resolveRoutePermission + can). Overall per-request cost is
|
|
sub-millisecond for the crypto step on Edge infrastructure.
|
|
Verdict: No performance finding.
|
|
|
|
Hydration check:
|
|
ForgotPasswordForm: "use client", one useFormState + one useEffect + useRouter.
|
|
Hydration bundle: this component + PhoneInput + SubmitButton + next/navigation.
|
|
No heavy imports. Hydration cost: low.
|
|
VerifyOtpForm: "use client", useFormState + 3× useState + useTransition +
|
|
useRef + 2× useEffect. Hydration bundle: this component + SubmitButton + Toast.
|
|
No third-party libraries beyond react-dom hooks. Hydration cost: low-medium
|
|
(multiple hook initializations, but no large state trees).
|
|
CompleteResetForm: "use client", useFormState + 3× useState + useMemo +
|
|
useEffect + clsx. Hydration bundle: this component + PasswordField (×2) +
|
|
SubmitButton + Toast. No heavy libraries. Hydration cost: low-medium.
|
|
Server Component pages (forgot-password, verify-otp, reset-password): zero client
|
|
JS in the RSC payload for the page shell itself. Only their Client Component
|
|
children hydrate.
|
|
|
|
Areas checked and found clean (render/hydration):
|
|
- No useLayoutEffect (would cause SSR/client mismatch warnings).
|
|
- No useContext with deeply nested providers in these files.
|
|
- No large initialState objects that inflate the RSC/HTML payload.
|
|
- No React.lazy or dynamic() import in these files — not needed for auth
|
|
pages, which are infrequently visited and don't benefit from code-splitting
|
|
beyond what Next.js app-router does automatically per route segment.
|
|
- No prop-drilling of large objects.
|
|
- No setState calls in render (outside of effects/handlers).
|
|
|
|
|
|
=======================================================================
|
|
AUDIT 3: Animation Performance
|
|
=======================================================================
|
|
|
|
MEASURED — direct source analysis:
|
|
|
|
No animation or transition Tailwind classes found in any of the 7 feature files.
|
|
|
|
Checked for:
|
|
- animate-* : 0 occurrences
|
|
- transition-* : 0 occurrences
|
|
- duration-* : 0 occurrences
|
|
- ease-* : 0 occurrences
|
|
- delay-* : 0 occurrences
|
|
- motion-* : 0 occurrences
|
|
- will-change-* : 0 occurrences
|
|
- transform / translate / scale / rotate : 0 occurrences (outside implicit Tailwind reset)
|
|
- framer-motion, react-spring, or other animation libraries : not imported
|
|
|
|
The resend-button countdown string update (`Resend code in Ns`) causes a DOM text-node
|
|
update every second — this is a data update, not an animation. No compositor
|
|
layers, no paint, no reflow beyond the text node. Cost is negligible.
|
|
|
|
The strength-bar color toggle (clsx bg-status-success / bg-border-subtle) on each
|
|
keystroke changes a computed style but triggers no layout (background-color changes
|
|
are paint-only, not layout). No compositor promotion; paint is confined to 4 small
|
|
bar elements.
|
|
|
|
Verdict: No animation performance findings.
|
|
|
|
|
|
=======================================================================
|
|
INFERRED (not measured) — clearly labeled
|
|
=======================================================================
|
|
|
|
INFERRED-1: PasswordField onChange re-renders
|
|
Both PasswordField instances in CompleteResetForm call onChange (setNewPassword,
|
|
setConfirmPassword) on every keypress. Each calls the parent state setter,
|
|
triggering a full CompleteResetForm re-render. The PasswordField components
|
|
themselves are controlled (they receive value). Without React.memo on PasswordField,
|
|
both fields re-render on every keystroke even when only one is being typed into.
|
|
Impact estimate: low — the component tree is shallow (2 PasswordField + 1 strength
|
|
section + 1 policy card). A profiler run would be needed to confirm any jank.
|
|
This is a plausible optimization target if typing latency is ever observed, but
|
|
NOT a confirmed regression from this code path alone.
|
|
Confidence: inferred (no profiler data; no prior baseline to compare against).
|
|
|
|
INFERRED-2: getResetFlowState cold-start latency
|
|
On the verify-otp and reset-password pages, if the DB connection pool is cold
|
|
(first request after inactivity), getResetFlowState may add 50–200ms to TTFB.
|
|
This is not observable from the source files alone and is an infrastructure-level
|
|
concern, not a code-level one.
|
|
Confidence: inferred.
|
|
|
|
|
|
=======================================================================
|
|
SUMMARY
|
|
=======================================================================
|
|
|
|
Measured findings: 0 blocking, 0 warn-level
|
|
Inferred findings: 2 (both low-confidence, low-impact — included for completeness)
|
|
Animation findings: 0
|
|
|
|
Bundle analysis: Build failed upstream (lib/actions/forgot-password.ts "use server"
|
|
constant export) — bundle delta unconfirmed. Feature files themselves contain no
|
|
heavy imports.
|
|
|
|
GATE STATUS: PASS
|
|
No real (measured) CSS, render/hydration, or animation performance issues found
|
|
in the 7 feature files. All findings are informational or inferred.
|