在每一次技术选型讨论会上,GraphQL 都是那个最耀眼的技术选型。
它的来历很漂亮:Facebook 出品,类型安全,按需取数据,一个端点解决所有请求,前端再也不用求着后端加字段。仅仅是听完这些卖点,你就已经开始在脑子里盘算怎么说服老板全面切换了。
但如果你真的在生产环境里大规模落地过 GraphQL,你大概率会经历一个极其痛苦的现实:理论上的每一个优势,在真实的工程现场都对应着一个等量甚至更大的代价。
尤其是在国内的技术环境下,这些代价尤其致命。
下面咱们细聊👇
HTTP 缓存体系彻底失效
REST 最容易被忽视、但极其重要的一个优势是:它天然和整个 HTTP 缓存基础设施兼容。
一个 GET /api/products/123 的请求,从浏览器到 CDN 到 Nginx 到 Redis,每一层都可以根据 URL 做精确的缓存。Cache-Control、ETag、Last-Modified——整个互联网几十年积累的缓存协议,全部是为 REST 风格的资源化 URL 设计的。
而 GraphQL 把所有请求都塞进了一个端点:POST /graphql。
每一个查询的请求体是不同的 JSON,但 URL 永远相同。这意味着:
- 浏览器的 HTTP 缓存完全失效——因为
POST请求默认不被缓存 - CDN 缓存完全失效——
CDN靠 URL 区分资源,所有GraphQL请求的 URL 都一样 - Nginx 的反向代理缓存完全失效——同理
为了弥补这个缺陷,你必须在应用层自己搭建一套缓存方案。Apollo Client 的 InMemoryCache 是目前最常用的客户端缓存方案,但它的复杂度足以让一个中级前端工程师崩溃:
// Apollo Client 的缓存配置:你需要手动告诉它如何识别和合并每一种类型的数据
const cache = new InMemoryCache({
typePolicies: {
Product: {
// 告诉缓存用 id 字段作为唯一标识
keyFields: ['id'],
fields: {
// 分页列表的缓存合并策略——你必须手写
reviews: {
keyArgs: ['sortBy'],
merge(existing = [], incoming, { args }) {
if (args?.offset === 0) return incoming;
return [...existing, ...incoming];
},
},
},
},
Query: {
fields: {
products: {
keyArgs: ['category', 'sortBy'],
merge(existing, incoming, { args }) {
// 游标分页的缓存合并,又是一段手写逻辑
const merged = existing ? { ...existing } : { edges: [] };
merged.edges = [...(merged.edges || []), ...incoming.edges];
merged.pageInfo = incoming.pageInfo;
return merged;
},
},
},
},
},
});
而同样的功能,REST + React Query(TanStack Query)只需要:
// REST + React Query:缓存由 URL 自动管理,零配置
const { data } = useQuery({
queryKey: ['products', { category, sortBy, page }],
queryFn: () => fetch(`/api/products?category=${category}&sort=${sortBy}&page=${page}`).then(r => r.json()),
staleTime: 5 * 60 * 1000, // 5 分钟内直接用缓存
});
queryKey 就是缓存键,自动去重、自动失效、自动垃圾回收。不需要写任何 typePolicies,不需要手动合并分页数据。
你在 GraphQL 上花大量精力解决的缓存问题,在 REST 里根本就不是问题🫡。
N+1 问题并没有消失
GraphQL 最大的宣传卖点之一是按需取数据,不多不少。前端需要什么字段,Query 里写什么字段,后端只返回这些字段。
听起来极其美好,但在后端的 Resolver(解析器)实现层面,这个按需带来了一个臭名昭著的性能灾难——N+1 查询问题。
假设前端发了这样一个查询:
query {
posts(limit: 20) {
id
title
author {
name
avatarUrl
}
comments(limit: 5) {
content
user {
name
}
}
}
}
看起来只是一个请求。但在后端的 Resolver 链中,实际执行的数据库查询可能是:
1 次查询:获取 20 篇文章
20 次查询:每篇文章获取作者信息
20 次查询:每篇文章获取 5 条评论
100 次查询:每条评论获取用户信息
——————————————————
总计:141 次数据库查询
一个前端请求,后端打了 141 次数据库。
解决方案是引入 DataLoader 做批量合并和缓存:
// DataLoader:把 N 次单条查询,合并为 1 次批量查询
const userLoader = new DataLoader(async (userIds: readonly string[]) => {
const users = await db
.select()
.from(usersTable)
.where(inArray(usersTable.id, [...userIds]));
// 必须保证返回顺序和传入的 ID 顺序严格一致
const userMap = new Map(users.map(u => [u.id, u]));
return userIds.map(id => userMap.get(id) ?? null);
});
DataLoader 确实能解决 N+1 问题,但它带来了新的复杂度:
- 每一种关联关系都需要创建对应的
Loader Loader的生命周期必须严格绑定到单次请求(否则跨请求数据污染)- 嵌套层级越深,
Loader的编排越复杂 - 调试极其困难——你很难追踪一个
GraphQL查询到底触发了多少次数据库调用
但在 REST 里,同样的数据需求,后端直接写一个带 JOIN 的 SQL,一次查询搞定,逻辑清晰、性能可控、调试又简单。
后端团队的抗拒
这一点在国内的技术环境下尤其致命。
GraphQL 的落地,不仅是前端的事——它要求后端团队彻底改变 API 的设计范式。从我给你什么你就用什么的推送模式,变成你要什么我都得能给的按需模式。
这意味着后端需要大量的接口改造!
在国内大多数团队里,后端工程师的 KPI 是什么?是按时交付业务需求。任何不直接产出业务价值的技术改造,在后端团队的优先级里都排在最末尾。 而 GraphQL 的落地,恰恰需要后端投入大量的时间来重构现有的 API 层。
我见过太多这样的场景:前端团队兴高采烈地推动 GraphQL 选型,后端团队勉强配合搭了一个 Schema,但只是在现有 REST 接口外面套了一层 GraphQL 的壳子——每一个 Resolver 里面直接调用原来的 REST 接口。
结果就是,你用 GraphQL 的复杂度,换来了 REST 的能力,还多了一层转发的延迟。 这种伪 GraphQL比纯 REST 更差😖。
为了类型安全?
GraphQL 的另一个卖点是 Schema 天然带类型,前端可以根据 Schema 自动生成 TypeScript 类型。
这在 2020 年确实是一个强大的优势。但到了 2026 年,REST 阵营的类型安全方案已经极其成熟:
- 后端用
Swagger/OpenAPI定义接口规范 - 前端用
openapi-typescript一键生成完整的TypeScript类型 - 配合
React Query+Zod做运行时校验
// 基于 OpenAPI 自动生成的类型 + React Query:零手写类型,完整类型安全
import type { paths } from './generated/api-types';
type ProductListResponse = paths['/api/products']['get']['responses']['200']['content']['application/json'];
const { data } = useQuery<ProductListResponse>({
queryKey: ['products', category],
queryFn: () => fetch(`/api/products?category=${category}`).then(r => r.json()),
});
// data 的类型完全自动推断,和 GraphQL codegen 的体验几乎一致
GraphQL 在类型安全上的领先优势,已经被 REST + OpenAPI + TypeScript 的工具链完全追平。 而后者的落地成本低了一个数量级。
那 GraphQL 什么时候真的值得用?
说了这么多缺点,GraphQL 真的一无是处吗?也不是。但它的适用场景极其狭窄:
但请注意!上面☝️两种场景,在国内绝大多数业务团队中极其罕见。大多数国内团队做的是中后台管理系统、营销活动页、电商交易流程——这些场景的数据结构是相对固定的,前后端的数据契约是稳定的,REST 的简单直接才是最优解。
国内团队的真实最优解:REST + BFF
经过大量团队的试错,国内前端圈逐渐收敛到了一个务实的方案:
前端不直接对接后端微服务,而是通过一个 BFF(Backend For Frontend)层做数据聚合和裁剪。
用户浏览器 → BFF(前端团队维护) → 后端微服务 A / B / C
BFF 由前端团队用 Node.js 或 Cloudflare Workers 维护。它的职责极其明确:
这个方案用 REST 的简单性,实现了 GraphQL 最核心的两个卖点(按需取数据、数据聚合),同时完全不触碰后端的现有架构,不需要后端团队做任何改造。
它不优雅,但它很管用😃。
怎么做出选择
GraphQL 的理论上限极高,但它的落地成本——缓存体系重建、N+1 治理、后端团队改造、工具链学习——对大多数国内团队来说,远远超出了它带来的收益🤔。
选技术不是选信仰。所以不要因为一个技术的 PPT 很漂亮,就忽略了它的落地代价。真正成熟的技术判断力,是在充分理解一个技术的优势之后,依然能冷静地问出那个最关键的问题:
在我们这个团队、这个业务、这个阶段,这个代价我们付得起吗?
喜欢我的文章,也欢迎关注我的微信公众号:【前端技术官】。
主要分享:前端架构 · AI 编程 · 职场认知 · 开发者成长
微信扫码关注 👆
不定期更新,不刷屏,聊点真正有用的干货。