Skip to main content

Performance Improvements — TODO Checklist (non-LCP)

Status: Draft · Author: AI Review · Date: 2026-07-06

Actionable checklist for app-wide performance and tuning work. Every item was verified against the current codebase (file references inline). The i18n item has its own step-by-step doc: i18n-message-splitting.md.

The cold-load LCP work (streaming, layout waterfall, Clerk membership, middleware round trip) shipped in July 2026 and is documented in lcp-strategy.md.

Items are ordered for execution: measurement first, then quick wins, then by impact-per-effort. Re-measure after each block before starting the next.


0. Baselines (do first, everything else is judged against this)

  • 0.1 Bundle baseline — done 2026-07-07 via npm run build:analyze; recorded in bundle-baseline-2026-07.md. Next 16 removed per-route First Load JS from build output; numbers computed from build-manifest.json. Headline: shared first-load JS 420 KB raw / 126 KB gz; whole client build 243 chunks / ~3.2 MB raw. Monaco confirmed async-only (not in any route's first load).
  • 0.2 Real-user metrics@vercel/speed-insights is out (staying on the Hobby plan; it's paid beyond the trial). Use useReportWebVitals in a small client component POSTing to a tiny API route that logs via pino; read metrics from Vercel logs. Capture LCP, INP, CLS, TTFB per route and tenant. Status 2026-07-07: not built yet; needs a few days of traffic before the baseline is usable.

1. Quick wins

  • 1.1 Move the activity-expiry sweep off the request path(app)/page.tsx:13 fires a Mongo write on every home view; replace with an idempotent cron-triggered route. Hobby-plan note: Vercel Cron only runs daily on Hobby — either daily is enough (expiry is date-based), or trigger the route hourly from a GitHub Actions schedule workflow (pattern already used for deploys).

2. Client bundle & payload

  • 2.1 i18n message splitting — stop shipping the full 60–68 KB catalog on every page; app-wide pick (~10–15 KB) + route-scoped nested providers. Mind the v4 inheritance gotcha (messages must be set explicitly). Detail: i18n-message-splitting.md (full step-by-step)
  • 2.2 Monaco / barrel audit — baseline (2026-07-07) shows Monaco is not in any route's first load (async chunk 8600, bundled locally, not CDN) → restoring next/dynamic is unnecessary. Remaining concern: chunk 8600 (757 KB raw) also carries sanitize-html + handlebars client-side — trace which client imports pull those in and whether they belong on the server.
  • 2.3 Per-locale Clerk localization import[locale]/layout.tsx:91-93 imports all ~50 locales per render; import only the active one. Baseline (2026-07-07): none of it reaches the client bundle — this is a server module-eval / RSC payload win only, so lower priority than first assumed.
  • 2.4 Small dependency candidates — baseline (2026-07-07): i18n-iso-countries is server-only (confirmed, no action); libphonenumber-js + jszip ship client-side in async chunk 1683 (370 KB raw) — use the libphonenumber subset entry point; qrcode, handlebars, sanitize-html are in client chunks (1863, 8600), contrary to the server-only assumption — trace and fix.

3. Server & data layer

  • 3.1 Mongo pool tuning — explicit maxPoolSize (~10) and serverSelectionTimeoutMS: 5000 in src/services/mongodb.ts; then delete the hand-rolled 5 s Promise.race in (app)/layout.tsx (one timeout mechanism, not two).
  • 3.2 Query projections.select() away logo buffers on the layout's org read (organizationLogoStrip proves the bytes are fetched then discarded); .explain() the getRopaByIdRO lookup (RopaSchema has no index on ropaId).
  • 3.3 withCache key hardeningunstable_cache keyed on [fn.name] risks cross-service collisions (multi-tenant data bleed); switch to explicit [serviceName, fn.name].

4. Rendering & caching strategy

  • 4.1 Legal/docs pages — cache the markdown render (unstable_cache) or move out of (app) for full static generation. Open product question: do legal pages need tenant navbar branding?
  • 4.2 Cache headers on public endpointss-maxage + stale-while-revalidate for /api/locale-default and api/gforms/locales/[locale]; copy the logo route's ETag pattern.
  • 4.3 React Compiler — enable reactCompiler in a dedicated branch; targets INP (auto-memoization of the large client trees). Highest regression risk of the list — full test pass + RUM comparison required.

5. Heavy endpoints

  • 5.1 PDF caching — cache generated PDFs keyed on content hash (reuse the templatePreview TTL-index storage pattern), invalidate via existing tenant tags; escalate to publish-time generation only if p95 still hurts. Avoids the multi-second Chromium cold start per request.
  • 5.2 maxDuration on import/export routes — explicit route segment config so large org imports fail loudly, not at the platform default timeout.

6. Guardrails & housekeeping

  • 6.1 Bundle-size CI check — fail PRs on First Load JS regression over a threshold (routine npm update commits can silently regress chunks).
  • 6.2 Confirm pool options flow through instrumentation.ts pre-warm (single connectDB entry point should guarantee it).
  • 6.3 nanoid v3 → v5 — ESM-only, smaller; low priority.

Explicitly out of scope

  • Cold-load LCP items — shipped, see lcp-strategy.md.
  • Region co-location (owner already aware/handled).
  • PPR / dynamicIO / cacheComponents experimental flags — revisit only if the above doesn't hit targets.
  • Next.js 16.3 "instant navigations" — improves client-side transitions, not cold-load LCP; upgrade on its own merits, independent of this list.
  • Image/font optimization — audited, already correct (next/font via geist, next/image proper, logo route ETag/immutable).