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.
