如何用 tRPC lazy 动态加载路由减少 serverless 冷启动开销
【免费下载链接】trpc🧙♀️ Move Fast and Break Nothing. End-to-end typesafe APIs made easy.项目地址: https://gitcode.com/GitHub_Trending/tr/trpc
当 tRPC 项目的 API 拆成多个 router 之后,如果入口文件把所有 router 一次性import进来,应用每次冷启动都会加载全部模块。官方文档在 Merging Routers 一节的 Dynamically load routers 中给出的方案是:用@trpc/server导出的lazy函数动态加载 router 模块,按需加载后再挂载到应用 router 上。文档原话是:
You can use the
lazyfunction to dynamically load your routers. This can be useful to reduce cold starts of your application. There's no difference in how you use the router after it's been lazy loaded vs. how you use a normal router.
也就是说:lazy用于把重 router 移出启动路径,只在实际被调用时才加载;加载完成之后,客户端调用方式和类型推导与普通 router 完全一致。
lazy 的两种写法
routers/_app.ts 中的示例展示了两种写法,区别在于模块里导出几个 router:
import { lazy } from '@trpc/server'; import { router } from '../trpc'; export const appRouter = router({ // Option 1: Short-hand when the module has exactly 1 router exported greeting: lazy(() => import('./greeting.js')), // Option 2: if exporting more than 1 router user: lazy(() => import('./user.js').then((m) => m.userRouter)), }); export type AppRouter = typeof appRouter;- 写法 1 是简写:模块只导出一个 router 时,直接把
import()的 Promise 传给lazy; - 写法 2 用于模块导出多个 router 的情况,用
.then((m) => m.userRouter)从模块里取出目标 router; - 最后一行
export type AppRouter = typeof appRouter保留不变,客户端照样用这个类型做端到端类型推导。
lazy的实现位于 packages/server/src/unstable-core-do-not-import/router.ts。
官方完整示例:examples/lazy-load
仓库里的 examples/lazy-load 是一个可运行的最小示例,README 注明环境要求:Requires node 18 (for global fetch)。示例的 tsconfig.json 使用"target": "esnext"、"module": "NodeNext",package.json 声明了"type": "module",所以动态import()的模块内都带.js扩展名。
先看入口如何组装 router,src/server/routers/_app.ts:
import { lazy } from '@trpc/server'; import { router } from '../trpc.js'; export const appRouter = router({ user: lazy(() => import('./user.js')), // Alternative way to lazy load slow: lazy(() => import('./slow.js')), }); export type AppRouter = typeof appRouter;tRPC 初始化在 src/server/trpc.ts,每个 router 模块从这里复用router和publicProcedure:
import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const router = t.router; export const publicProcedure = t.procedure;示例中最能说明“按需加载”的是 src/server/routers/slow.ts——它把日志和一段 3 秒延迟写在模块顶层,用来模拟一个加载很慢的模块:
import { publicProcedure, router } from '../trpc.js'; console.log('💤 Lazy loading slow router...'); await new Promise((resolve) => setTimeout(resolve, 3000)); export const slowRouter = router({ hello: publicProcedure.query(() => 'world'), });由于slow.js只在lazy(() => import('./slow.js'))被触发时才执行import(),这些顶层日志和延迟不会在应用启动时发生,只在第一次加载该模块时发生。src/server/routers/user.ts 是普通 router(list/byId/create三个 procedure,查一个内置的假数据库),同样被包在lazy里。
服务端入口 src/server/index.ts 用 standalone adapter 在 3000 端口起 HTTP 服务:
import { createHTTPServer } from '@trpc/server/adapters/standalone'; import { appRouter } from './routers/_app.js'; const server = createHTTPServer({ router: appRouter, }); server.listen(3000);客户端 src/client/index.ts 只从服务端导入AppRouter类型,调用方式和普通 router 没有任何区别:
import { createTRPCClient, httpBatchLink } from '@trpc/client'; import type { AppRouter } from '../server/routers/_app.js'; const trpc = createTRPCClient<AppRouter>({ links: [ httpBatchLink({ url: 'http://localhost:3000', }), ], }); const hello = await trpc.slow.hello.query(); console.log('slow.hello:', hello);运行与验证
在examples/lazy-load目录下,按 README 给出的命令执行:
npm i npm run devdev脚本(见 package.json)会并行跑dev:server(tsx watch src/server)和dev:client(等待 3000 端口后tsx watch src/client)。运行成功时客户端会依次打印查询结果,其中slow.hello返回world(文档示例输出)。观察 lazy 加载是否生效,看两点:
- 服务启动阶段不会出现
slow.ts顶层的💤 Lazy loading slow router...日志; - 当客户端第一次调用
trpc.slow.hello.query()触发模块加载时,该日志才出现,且这次调用会额外等待模块顶层的 3 秒模拟延迟(这是示例刻意写入的,真实项目里对应的是慢模块自身的加载成本)。
如果要验证构建产物,示例的build脚本就是tsc。这里有一处文档不一致需要留意:README 的 Building 一节写的是npm run build+npm run start,但 package.json 中并没有定义start脚本,实际可用的是build、dev、test-dev(start-server-and-test 'tsx src/server' 3000 'tsx src/client')和test-start(start-server-and-test 'node dist/server' 3000 'node dist/client')。要跑构建后的验证,用npm run build后再执行npm run test-start即可。
限制与注意点
lazy简写只适用于“模块恰好导出 1 个 router”的情况;一个模块导出多个 router 时必须用.then((m) => m.xxxRouter)显式取出口。- 文档示例对
lazy代码块标注了@target: esnext,即动态import()需要较新的编译目标;官方示例的 tsconfig 是esnext+NodeNext,且以 ESM("type": "module"、.js扩展名)方式书写。 lazy只是把模块加载推迟到首次调用,文档明确说的是“useful to reduce cold starts”,即它减少的是启动时必须执行的模块;router 内部依赖(如数据库连接)在首次调用时的初始化成本不会因此消失,示例中 3 秒延迟就是用来演示这部分成本的。
下一步如果要把 API 继续拆细,可参考 merging-routers 文档 中 child routers 与t.mergeRouters的两种组织方式,再对其中体积较大的模块套用本文的lazy写法。
【免费下载链接】trpc🧙♀️ Move Fast and Break Nothing. End-to-end typesafe APIs made easy.项目地址: https://gitcode.com/GitHub_Trending/tr/trpc
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考