5月27日 22:26
GraphQL 性能优化有哪些策略?
核心答案
GraphQL 性能优化的关键在于六个方向:解决 N+1 查询、限制查询复杂度、缓存策略、查询持久化、分页优化、监控分析。其中 N+1 问题是面试最高频考点。
N+1 查询问题与 DataLoader
查询嵌套关系时,每个父对象都触发一次子查询,10 篇文章就产生 10 次 author 查询。DataLoader 通过批量 + 缓存解决:
javascriptconst DataLoader = require('dataloader'); const userLoader = new DataLoader(async (ids) => { const users = await User.findAll({ where: { id: ids } }); return ids.map(id => users.find(u => u.id === id)); }); // Resolver 中 Post: { author: (post) => userLoader.load(post.authorId) }
DataLoader 在单次 tick 内收集所有 load 调用,合并为一次批量查询,相同 id 自动去重。
追问:DataLoader 的缓存策略在什么场景下会失效? 当跨请求复用同一 DataLoader 实例时,缓存会返回脏数据。正确做法是每个请求创建新实例。
查询深度与复杂度限制
恶意查询可以无限嵌套,必须设防:
javascriptconst depthLimit = require('graphql-depth-limit'); const { createComplexityLimitRule } = require('graphql-validation-complexity'); const server = new ApolloServer({ typeDefs, resolvers, validationRules: [ depthLimit(7), createComplexityLimitRule(1000) ] });
深度限制防嵌套炸弹,复杂度限制防字段爆炸。两者缺一不可。
缓存策略
三层缓存各有分工:
- 字段级 Redis 缓存:对 Resolver 结果按
fieldName:args做 TTL 缓存,适合读多写少的数据 - Apollo Client 缓存:客户端用
InMemoryCache做 normalized 缓存,按__typename:id去重 - CDN 缓存:配合持久化查询(见下文),GET 请求可直接走 CDN
查询持久化
客户端将查询文本算 hash,服务端只传 hash 执行:
javascriptconst { createPersistedQueryLink } = require('@apollo/client/link/persisted-queries'); const { sha256 } = require('crypto-hash'); const link = createPersistedQueryLink({ sha256, useGETForHashedQueries: true });
好处:请求体从几 KB 缩到几十字节,可走 GET + CDN 缓存,还降低了查询被篡改的风险。
游标分页
offset 分页在数据变更时会出现重复或遗漏,游标分页用有序字段(如 id、createdAt)做游标,稳定性不受数据量影响:
graphqltype Query { posts(after: String, first: Int): PostConnection! }
监控与分析
上线前接 Apollo Studio 或自建 metrics,记录每个 Resolver 的耗时和错误率。性能问题不是"有没有"的问题,而是"在哪"的问题,没有监控就是盲调。
追问:如果 Resolver 耗时突然翻倍,你会从哪些方向排查? 先看数据库慢查询日志,再检查 DataLoader 是否失效(缓存击穿),最后确认是否有新增的深嵌套查询绕过了复杂度限制。