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 inbundle-baseline-2026-07.md. Next 16 removed per-route First Load JS from build output; numbers computed frombuild-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-insightsis out (staying on the Hobby plan; it's paid beyond the trial). UseuseReportWebVitalsin 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:13fires 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 Actionsscheduleworkflow (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 (
messagesmust 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) → restoringnext/dynamicis unnecessary. Remaining concern: chunk8600(757 KB raw) also carriessanitize-html+handlebarsclient-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-93imports 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-countriesis server-only (confirmed, no action);libphonenumber-js+jszipship client-side in async chunk1683(370 KB raw) — use the libphonenumber subset entry point;qrcode,handlebars,sanitize-htmlare 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) andserverSelectionTimeoutMS: 5000insrc/services/mongodb.ts; then delete the hand-rolled 5 sPromise.racein(app)/layout.tsx(one timeout mechanism, not two). - 3.2 Query projections —
.select()away logo buffers on the layout's org read (organizationLogoStripproves the bytes are fetched then discarded);.explain()thegetRopaByIdROlookup (RopaSchemahas no index onropaId). - 3.3
withCachekey hardening —unstable_cachekeyed 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 endpoints —
s-maxage+stale-while-revalidatefor/api/locale-defaultandapi/gforms/locales/[locale]; copy the logo route's ETag pattern. - 4.3 React Compiler — enable
reactCompilerin 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
templatePreviewTTL-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
maxDurationon 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 updatecommits can silently regress chunks). - 6.2 Confirm pool options flow through
instrumentation.tspre-warm (singleconnectDBentry point should guarantee it). - 6.3
nanoidv3 → 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/cacheComponentsexperimental 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/fontvia geist,next/imageproper, logo route ETag/immutable).