- 后端
- GraphQL
- API设计
【免费下载链接】type-graphql
Create GraphQL schema and resolvers with TypeScript, using classes and decorators!
导读
TypeGraphQL 是基于graphql-js的 TypeScript 类型化抽象层,它在带来类与装饰器开发体验的同时,也引入了可衡量的运行时开销。本篇指南以 docs/performance.md 为核心,结合仓库内 benchmarks 基准测试与 schema-generator.ts 源码实现,系统说明:抽象层开销从何而来、Promise 异步执行路径为何是主要瓶颈、{ simple: true }与{ simpleResolvers: true }优化开关的原理与代价,以及如何在真实项目中安全地应用这些优化。
TypeGraphQL 抽象层与性能权衡的由来
TypeGraphQL本质上是在 JavaScript 参考实现graphql-js之上构建的一层抽象。它不仅允许开发者用类(class)和装饰器(decorator)来构建 GraphQL schema,还提供了一整套专注于开发体验的工具链——授权(authorization)、校验(validation)、自定义中间件(middlewares)等,让常见任务变得简单易做。
这种便利是有代价的:抽象层本身会带来一定的运行时性能开销。文档在 docs/performance.md 开篇即明确指出:在追求易用与便捷开发的同时,性能上有时需要做出权衡。因此,理解这些开销的具体来源、测量方式以及内置的优化手段,是构建高性能 GraphQL API 的关键前提。
Benchmarks:如何测量抽象层的额外开销
为了量化抽象层带来的开销,仓库在 benchmarks 目录下提供了一套可复现的对比基准,将 TypeGraphQL 与"裸金属"的原始graphql-js实现进行对照。
基准测试的运行机制
以数组场景为例,benchmarks/array/run.ts 定义了统一的测试框架:
- 固定返回25,000 个嵌套对象(
ARRAY_ITEMS = 25000)的查询; - 执行50 次迭代(
BENCHMARK_ITERATIONS = 50)并计时; - 每次执行后对返回结果做断言校验(数据非空、数组长度正确、嵌套字段值正确),确保测的是"正确执行"而非空转。
测试查询固定为嵌套字段选择:
query { multipleNestedObjects { stringField booleanField numberField nestedField { stringField booleanField numberField } } }最严苛场景下的实测数据
文档给出了在最严苛用例(返回 25,000 个嵌套对象)下与graphql-js的对比结果:
| 25 000 array items | Deeply nested object | |
|---|---|---|
| Standard TypeGraphQL | 1253.28 ms | 45.57 μs |
graphql-js | 265.52 ms | 24.22 μs |
可以看出,在极端情况下 TypeGraphQL 的执行时间大约是裸graphql-js的5 倍。仓库中 benchmarks/array/results.txt 记录了另一组完整实测(Core i7 2700K、Node.js v13.5、50 次迭代总耗时),同样印证了这一趋势:
| 场景 | 50 次迭代总耗时 |
|---|---|
| TypeGraphQL standard | 15.518 s |
| TypeGraphQL 使用 sync field resolvers | 18.180 s |
| TypeGraphQL 使用 async field resolvers | 39.934 s |
| TypeGraphQL 使用 getters | 31.207 s |
| TypeGraphQL standard + global middleware | 62.664 s |
TypeGraphQL 使用simpleResolvers | 14.980 s |
graphql-jsstandard | 13.276 s |
graphql-jsasync field resolvers | 25.630 s |
文档同时强调:在真实应用中(例如涉及复杂数据库查询时),开销系数通常远低于 5 倍,但仍然不可忽视。这正是 TypeGraphQL 内置多项性能优化选项的原因。
主要瓶颈:Promise 与异步执行路径
JavaScript 中的 Promise 具有相当可观的性能开销。文档给出了一个直观的对比:在同一个返回 25,000 项数组的示例中,如果把 Object Type 的字段 resolver 改成返回 Promise 的异步实现,即使是"裸"graphql-js,执行也会变慢约一半:
graphql-js | 25 000 array items |
|---|---|
| sync resolvers | 265.52 ms |
| async resolvers | 512.61 ms |
仓库中 benchmarks/array/graphql-js/async.ts 展示了这种异步实现方式——每个字段都声明resolve: async source => ...,而 benchmarks/array/graphql-js/standard.ts 则依赖graphql-js的隐式同步字段解析。对比结果可见:字段级异步解析的代价是全局性的,会波及整棵查询树的执行。
TypeGraphQL 的策略是:尽可能避免走异步执行路径。从文档的说明来看,当满足以下条件时,TypeGraphQL 会尝试跳过异步路径:
- 查询/变更/字段 resolver 未使用授权(auth)特性;
- 未使用参数(args),或参数校验已禁用;
- resolver 本身不返回 Promise。
因此,如果在应用中发现瓶颈,文档建议从这三方面入手排查:审视自己的 resolvers、关闭未使用的特性、移除不必要的async/await用法。
这一策略在源码中也有对应体现。在 schema-generator.ts 生成字段配置时,字段的resolve函数会根据是否为"simple resolver"来选择不同的生成路径:
resolve: fieldResolverMetadata ? createAdvancedFieldResolver(fieldResolverMetadata) : isSimpleResolver ? undefined : createBasicFieldResolver(field),即:存在显式字段 resolver 元数据时走高级路径;启用 simple resolver 时直接不生成包装 resolver(交给graphql-js隐式解析,避免额外函数调用);否则才创建基础字段 resolver。从源码结构看,这套分支设计正是为了让高频、简单的字段避开抽象层的额外包装逻辑。
中间件对性能的额外影响
文档特别警告:使用中间件会隐式开启异步执行路径。对于全局中间件(global middlewares),中间件栈甚至会在每一个隐式字段 resolver 上创建,这意味着哪怕你没有显式声明字段 resolver,全字段都会背上中间件栈的负担。
仓库中的 benchmarks/array/type-graphql/with-global-middleware.ts 正是这一场景的实测用例——它注册了一个打印parentType.fieldName的loggingMiddleware作为globalMiddlewares。从 benchmarks/array/results.txt 可以看出,标准 TypeGraphQL 在加入全局中间件后,总耗时从 15.518 s 猛增至 62.664 s(约 4 倍)。这也是文档表格中"TypeGraphQL with a global middleware"高达 1253.28 ms 的原因。
因此,文档给出的建议是:如果非常在意性能,要谨慎使用中间件特性;如果确实需要中间件,可以配合下文介绍的 "simple resolvers" 技巧来抵消部分开销。文档还透露,整个中间件栈后续将以性能优先为原则重新设计,并引入允许细粒度控制全局中间件作用范围的新 API。
进一步优化:simple与simpleResolvers开关
当查询返回大量 JSON 形态的数据,且不需要任何字段级访问控制或自定义中间件时,可以关闭整条授权与中间件链。TypeGraphQL 提供了两个粒度的装饰器选项。
字段级:{ simple: true }
对选定的字段 resolver 单独关闭授权与中间件栈:
@ObjectType() class SampleObject { @Field() sampleField: string; @Field({ simple: true }) publicFrequentlyQueriedField: SomeType; }类型级:{ simpleResolvers: true }
将该行为应用到整个 Object Type 的所有字段:
@ObjectType({ simpleResolvers: true }) class Post { @Field() title: string; @Field() createdAt: Date; @Field() isPublished: boolean; }仓库中 benchmarks/array/type-graphql/simple-resolvers.ts 给出了一个完整的实战示例:@ObjectType({ simpleResolvers: true })声明在SampleObject上,同时buildSchema仍注册了全局loggingMiddleware,用于验证"开启 simpleResolvers 后中间件是否还会执行"这一行为差异。
源码层面的判定逻辑
在 schema-generator.ts,isSimpleResolver的判定优先级是:字段级simple显式配置 > 类型级simpleResolvers配置 > 默认关闭:
const isSimpleResolver = field.simple !== undefined ? field.simple === true : objectType.simpleResolvers !== undefined ? objectType.simpleResolvers === true : false;这一实现意味着:即使类型上未开启simpleResolvers,你也可以用@Field({ simple: true })精准地对个别高频字段做"豁免";反之,类型级开关开启后,个别字段也可通过@Field({ simple: false })显式恢复完整执行链。
simpleResolvers 的实际收益
文档给出的基准数据显示,这个简单技巧最高可以将执行提速76%——使用 simple resolvers 后的执行速度几乎与裸graphql-js相当,实测额外开销仅约13%,远优于默认状态下的 500%:
| 25 000 array items | |
|---|---|
graphql-js | 265.52 ms |
| Standard TypeGraphQL | 310.36 ms |
| TypeGraphQL with a global middleware | 1253.28 ms |
| TypeGraphQL with "simpleResolvers" applied (and a global middleware) | 299.61 ms |
仓库中的 benchmarks/array/results.txt 也独立验证了这一结论:在挂载全局中间件的前提下,开启simpleResolvers后总耗时从 62.664 s 回落到 14.980 s,甚至低于标准 TypeGraphQL(15.518 s),非常接近裸graphql-js的 13.276 s。
注意:这一优化默认不开启,主要原因是全局中间件与授权(authorization)特性默认生效,而 simple resolvers 会绕过它们。
权衡与注意事项:什么情况下才该使用
文档强调,使用 "simple resolvers" 意味着主动关闭这些能力,必须清楚其后果:
@Authorized守卫失效:字段上的授权守卫不再生效,该字段会变成公开可用;- 全局中间件不执行:该字段不会经过全局中间件,可能丢失性能指标采集、访问日志等功能。
正因为如此,文档给出的经验法则是:只有在确实需要时才使用 simple resolvers,典型场景就是返回海量嵌套对象的数组(如本次基准测试的 25,000 项)。在常规业务字段上滥用这一开关,会以牺牲安全性与可观测性为代价,得不偿失。
从仓库的基准数据看,这一权衡的必要性也很清晰:标准 TypeGraphQL 在数组场景的额外开销约为 17%(310.36 ms vs 265.52 ms),尚属可接受范围;只有当叠加全局中间件导致开销放大到约 4.7 倍(1253.28 ms)时,simple resolvers 才成为必要的性能补救手段。
实战排查清单
综合文档与仓库源码,可以归纳出一份可操作的性能排查清单:
- 测量先行:参考 benchmarks/array/run.ts 的框架,用固定查询与多次迭代量化 schema 的真实吞吐与延迟,而非凭感觉判断瓶颈;
- 检查异步滥用:优先排查是否存在不必要的
async/await与 Promise 返回——即使去掉后仅省下"一半",在 25,000 项级别的数据量上也是数量级的差异(见 benchmarks/array/results.txt:async field resolvers 39.934 s vs sync 18.180 s); - 审视中间件范围:全局中间件会作用于每个隐式字段 resolver,评估其必要性,必要时改为更小粒度;同时关注文档提及的中间件栈性能重构与细粒度作用域新 API 的后续进展;
- 精准豁免高频字段:对只读、公开、返回大量嵌套数据的字段使用
@Field({ simple: true });对整类"胖"对象使用@ObjectType({ simpleResolvers: true }),并用{ simple: false }显式保护需要授权/中间件的字段; - 关注编译目标:benchmarks/simple/results.txt 显示,同样 100,000 次迭代下,ES2018 构建(nestedObject 4.557 s)显著优于 ES2016 构建(17.068 s),说明 TypeScript 编译目标(
tsconfig的target)也会影响运行时性能,值得纳入优化范围。
通过上述手段,可以在保留 TypeGraphQL 开发体验的同时,把抽象层开销控制在与裸graphql-js相近的水平。
- 后端
- GraphQL
- API设计
【免费下载链接】type-graphql
Create GraphQL schema and resolvers with TypeScript, using classes and decorators!
相关推荐
EDR-Telemetry项目实战:使用遥测生成器测试你的安全防护
EDR Telemetry项目实战:使用遥测生成器测试你的安全防护 EDR Telemetry是一款功能强大的开源项目,旨在帮助安全专业人员比较和评估各种EDR
WordPress数据库抽象层:wpdb类与SQL查询优化终极指南
WordPress数据库抽象层:wpdb类与SQL查询优化终极指南 WordPress作为全球最流行的内容管理系统,其强大的数据库抽象层wpdb类是保障网站性能
后端CMSTypeGraphQL查询缓存:优化重复查询性能
TypeGraphQL查询缓存:优化重复查询性能 在GraphQL应用开发中,重复查询导致的性能问题常常困扰开发者。当多个用户或客户端频繁请求相同数据时,未优化
后端GraphQLAPI设计
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考