Bump @trpc/* to ^11.17.0 and @tanstack/react-query(+devtools) to ^5.101.0.
Root config (src/utils/trpc.ts, src/server/trpc.ts):
- CreateTRPCNext takes 2 generics; type CreateTRPCProxyClient -> CreateTRPCClient
- transformer moves onto the link; createTRPCNext also needs it as a top-level option
- createCallerFactory off `t`; middleware rawInput -> async getRawInput()
React Query v5:
- mutation .isLoading -> .isPending (380 sites); isPreviousData -> isPlaceholderData
- client cacheTime -> gcTime (server cacheTime vars left untouched)
- keepPreviousData -> placeholderData via shared withPlaceholderData() helper + RQ fn
- query onSuccess/onError/onSettled removed -> useEffect (or awaited refetch)
- refetchInterval callback now receives the Query (query.state.data)
- useIsMutating(key) -> useIsMutating({ mutationKey }); getNextPageParam :0 -> :undefined
- DefaultErrorShape -> TRPCDefaultErrorShape; ReactQueryDevtools position -> buttonPosition
typecheck clean (0 errors). Routers kept eager; router.lazy() is a later phase.
See docs/trpc-v11-migration-plan.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
7.9 KiB
tRPC route dev-recompile bottleneck — findings & tRPC v11 upgrade notes
Context for whoever picks up the tRPC v10 → v11 upgrade. This summarizes a session that investigated slow
next devrecompiles and prototyped a fix. Self-contained — no prior context needed.
TL;DR
- Symptom: editing any file under
src/server/services/*triggers a recompile of the entire/api/trpc/[trpc]route — ~6,255 modules, ~21s cold / ~8.8s warm route rebuild — becausepages/api/trpc/[trpc].ts→appRouter(src/server/routers/index.ts) statically imports all ~93 routers → all their services into one compilation unit. - Root cause is architectural, not zod/schema init. (85 schema files / ~1,110 zod constructors were measured as a non-factor — zod construction is cheap.)
- The clean fix is tRPC v11 lazy routers (
router.lazy(() => import('./x.router'))), one line per router inindex.ts. This defers each router + its service subgraph into its own chunk, so editing a service only rebuilds that chunk, not the whole route. - We proved the mechanism works by manually lazy-loading one service in v10 (see below),
then reverted it — manual per-service
await import()is too invasive for full coverage and the real win is the v11 router-level version.
What was verified (v10 manual prototype)
We converted every importer of generation.service.ts (router + controllers + services it's
reached through) from static imports to lazy await import('...generation.service') at call
sites. Result, confirmed in dev logs:
| Edited file | Compiling /api/trpc/[trpc] line? |
Meaning |
|---|---|---|
generation.service.ts (converted → lazy) |
absent | detached from route's eager graph ✅ |
image.service.ts (unconverted → eager) |
present (✓ in 8.8s) |
still wired into the route |
Diagnostic signature (use this to verify any detachment): edit the service on a warm
server. If no ○ Compiling /api/trpc/[trpc] line appears, it's detached.
Caveats — read these before the v11 upgrade
1. ⚠️ Lazy-loading reorders module evaluation → can EXPOSE latent circular deps
This is the biggest risk for the v11 upgrade. Switching routers to lazy() changes which
module first triggers a given dependency cluster's evaluation. A cluster with a pre-existing
circular import that only worked due to a lucky eval order will start throwing at runtime:
TypeError: Cannot read properties of undefined (reading 'optional' | 'union' | ...)
…at module-top-level schema / DataGraph construction.
We hit exactly this: detaching generation.service surfaced a latent cycle in the data-graph:
src/shared/data-graph/generation/*-graph.ts → common.ts → config/index → config/workflows.ts → *-graph.ts
(config/workflows.ts imported version-id constants from the graph files, while the graphs
import helpers back from it — a bidirectional edge).
Because v11 lazy routers shift the eval order of every router, expect MORE of these to surface across the whole codebase, not just the data-graph. Plan for a cycle-fixing pass.
Mitigations / fix patterns:
- Enable the circular-dependency plugin first.
next.config.mjsalready contains a commented-outcircular-dependency-pluginblock; run withCIRCULAR_DEPENDENCY_PLUGIN=trueto map cycles at build time before they bite at runtime. Do this up front. - Break bidirectional edges by extracting shared leaf values (constants, schemas, enums)
into zero-import leaf modules. Example fix from this session: moved
klingVersionIds/nanoBananaVersionIds/viduVersionIdsout of the*-graph.tsfiles into a new leafsrc/shared/data-graph/generation/version-ids.ts; both the graphs andconfig/workflows.tsimport the leaf → edge becomes one-directional. (Graph files re-export the consts so existing handler imports keep working.) - Import leaf schemas from their source, not via a big module's re-export.
kling-graphimportedimageValueSchemathroughcommon.ts's re-export (commonis in the cycle); fixed by importing directly from the leafmedia-schemas.ts.
2. The re-seal floor — lazy routers won't make dev recompiles instant
Next.js webpack dev re-seals the entire server compilation on any server edit (you'll see a
bare Compiled in Xs (N modules) with no route name). There's an irreducible floor
(~4.4s warm in this codebase) regardless of how few modules changed. Lazy-loading removes the
extra route rebuild on top of the floor (the 8.8s), not the floor itself. Set expectations:
v11 lazy routers reduce per-edit cost meaningfully but don't eliminate it. If the floor itself
becomes the bottleneck, that's the point to revisit Turbopack (different incremental engine, no
re-seal) — see §4.
3. The (N modules) count in dev logs is NOT a per-edit delta
It's the total module count of the current compilation, and it grows as you visit more
pages in a session. Two recompiles showing 6255 vs 11888 modules are not comparable if
you've loaded different pages in between. To compare, hold session state constant and use
(a) presence/absence of the Compiling /api/trpc/[trpc] line and (b) wall-clock time.
4. Turbopack was explored and parked (alternative path, not pursued)
next dev --turbo on this repo boots but hits a series of incompatibilities. If revisited,
the known blockers are:
- SharedWorker imported via server-absolute path (
new URL('/src/workers/...', import.meta.url)) — Turbopack needs a relative path ('../../workers/...'). Works in webpack too. - CSS Modules
:globalnested inside native CSS nesting in*.module.css— Turbopack's Lightning CSS parser rejects it (css-loadertolerates it). Fix: don't nest:global {}; reference global keyframes by plain name (they resolve if defined outside the module). @civitai/clientERR_UNSUPPORTED_DIR_IMPORT— needstranspilePackages: ['@civitai/client']. But that also makes the cold compile slower (Turbopack then transpiles the large generated client).- Net: cold compile was not faster; warm was. Parked in favor of the v11 path.
5. Hot-path first-request latency
A lazily-loaded module pays a one-time chunk compile on the first request to its endpoint
after a server (re)start. Harmless in dev (one-time blip). If lazy routers are ever weighed for
prod cold-start, image.service (the feed hot path, ~7.9K lines) is the one to watch.
tRPC v11 upgrade specifics
- Current versions (all need bumping together):
@trpc/server,@trpc/client,@trpc/next,@trpc/react-query— all^10.45.0. - The dev-speed payoff is
router.lazy()— wire it insrc/server/routers/index.ts, ideally heaviest routers first (image, model, post, generation, orchestrator). - Follow the official v10→v11 migration guide for breaking changes (transformer config moved to
the link, etc.) — the
superjsontransformer setup insrc/server/trpc.tsand the client links will need updating. - Recommended sequence: (1) enable circular-dependency plugin & fix all reported cycles;
(2) do the v11 API migration with routers still eager and confirm green typecheck + app boot;
(3) only then convert routers to
lazy()incrementally, watching for the runtimeundefinedsignature from §1 as each is converted.
Quick reference — files touched in the (reverted) prototype
Server importers of generation.service that were converted: generation.router.ts,
model.controller.ts, model-version.controller.ts, recommenders.controller.ts,
model.service.ts, post.service.ts, orchestrator/common.ts, orchestration-new.service.ts,
orchestrator/queue-limits.ts, models.search-index.ts. Type-only client imports and
pages/api/** importers were left static (separate bundles; don't affect the tRPC route).
Watch for relative-path importers (../services/...) — a ~/server/... grep misses them.