一条 GraphQL 查询在测试时 200ms,生产偶尔超过 5 秒;增加 pageSize 后,耗时近似线性甚至成倍增长。若每返回一个商品,自定义 Resolver 都重新加载产品、库存或 ERP 数据,就形成 N+1:先查一次列表,再为 N 个节点各查一次。
不要只看总耗时
在测试环境为一次请求记录 SQL 数量、相同 SQL 模板出现次数、外部 HTTP 调用数和每个 Resolver 耗时。分别用 pageSize 1、10、50 比较。如果 SQL/调用次数随实体数等比例增长,证据比“服务器很忙”更明确。
Resolver 收到的 value 通常已有上下文
父 Resolver 可能已经提供 model 或所需字段。自定义字段若仅为了一个属性又调用 repository->getById,会重复加载。先查看 $value 中的 model 与已选字段,缺少数据时让集合层批量选择,而不是逐项补查。
public function resolve(Field $field, $context, ResolveInfo $info,
array $value = null, array $args = null) {
if (!isset($value['model'])) {
throw new LocalizedException(__('Product model is missing.'));
}
return $value['model']->getData('custom_label');
}
需要外部数据时使用批量思想
收集当前请求中的实体 ID,一次查询映射表或一次调用支持批量的外部 API,再按 ID 分发结果。Magento GraphQL 提供批量 Resolver 机制时,应按接口约定实现。数据加载器的缓存范围要限制在请求内,避免把客户专属值共享给另一请求。
缓存为何让同一查询忽快忽慢
GraphQL 响应和 Resolver 结果可能受缓存、身份标签、客户 token、store 与 currency 影响。匿名请求命中、登录请求绕过,或者自定义 Resolver 没提供正确 cache identity,会造成时间差。比较响应头和上下文,不要把一次暖缓存与一次冷缓存直接对比。
字段选择也决定成本
GraphQL 客户端可以请求大量嵌套字段。前端只显示标题和价格,却同时获取媒体库、可配置选项、库存明细和评论,会触发更多 provider。用最小查询逐个加字段,找到成本陡增的节点;为 pageSize 和查询复杂度设置合理限制,防止一个请求拖垮 PHP worker。
验证优化是否真实
优化前后在相同 store、客户身份、pageSize 和缓存状态下比较 P50/P95、SQL 数量和外部调用数。理想结果不是“50 个商品快了”,而是查询次数从随 N 增长变成近似固定。随后测试错误与超时,批量 API 部分失败时不能让所有实体静默返回错误数据。
观察生产请求但避免记录敏感变量
可按 operation name、字段集合哈希和耗时建立指标,避免把客户 token、邮箱、搜索词或完整 variables 写入普通日志。对慢请求采样时记录 Resolver 路径和批次大小,这样既能找到 N+1,也不会为了性能诊断扩大数据暴露面。
批处理 Key 必须包含业务维度
批量加载器若只按 product_id 缓存,却忽略 store、customer group、currency 或 website,会把一次请求中的不同上下文合并成错误结果。Key 至少要覆盖决定返回值的维度;客户专属价格和权限数据不应进入跨请求共享缓存。批次分发时还要保持输入顺序和缺失 ID 的占位,否则结果会错配到另一个商品。
外部服务要限制 fan-out
即使数据库查询已经批量化,Resolver 为每个商品并发调用 ERP 也会形成网络 N+1,并可能瞬间触发对方限流。优先使用批量端点;没有批量端点时设置请求级并发上限、短超时和明确的降级字段。不要在一个可选推荐字段失败时让整条商品查询返回 500。
性能回归测试可把商品数逐步提高到 1、10、25、50,绘制耗时与 SQL/HTTP 次数曲线。批量化后曲线仍随 N 急剧上升,说明还有嵌套字段或序列化成本未处理,而不是继续盲目增加缓存。

