跳到正文
掘金·· 8 小时前AI 评分28

GraphQL 在国内为什么水土不服?

AI 导读

GraphQL 在国内团队落地常遇到缓存、性能和协作成本,文章认为多数业务场景下 REST 更务实。GraphQL 的单一端点难以利用 HTTP 缓存,Resolver 还可能引发 N+1 查询;REST 则可结合 OpenAPI、TypeScript 和 React Query 实现类型安全与缓存。文章建议通过 BFF 聚合和裁剪数据,减少对后端现有架构的改造。

正文

GraphQL 在国内为什么水土不服?

在每一次技术选型讨论会上,GraphQL 都是那个最耀眼的技术选型。

它的来历很漂亮:Facebook 出品,类型安全,按需取数据,一个端点解决所有请求,前端再也不用求着后端加字段。仅仅是听完这些卖点,你就已经开始在脑子里盘算怎么说服老板全面切换了。

但如果你真的在生产环境里大规模落地过 GraphQL,你大概率会经历一个极其痛苦的现实:理论上的每一个优势,在真实的工程现场都对应着一个等量甚至更大的代价。

尤其是在国内的技术环境下,这些代价尤其致命。

下面咱们细聊👇


HTTP 缓存体系彻底失效

REST 最容易被忽视、但极其重要的一个优势是:它天然和整个 HTTP 缓存基础设施兼容。

一个 GET /api/products/123 的请求,从浏览器到 CDN 到 Nginx 到 Redis,每一层都可以根据 URL 做精确的缓存。Cache-Control、ETag、Last-Modified——整个互联网几十年积累的缓存协议,全部是为 REST 风格的资源化 URL 设计的。

REST 与 GraphQL 缓存对比.png

而 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 的设计范式。从我给你什么你就用什么的推送模式,变成你要什么我都得能给的按需模式。

这意味着后端需要大量的接口改造!

GraphQL理想与伪实现的落差.png

在国内大多数团队里,后端工程师的 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 真的一无是处吗?也不是。但它的适用场景极其狭窄:

GraphQL 与 REST 场景对比信息图.png

但请注意!上面☝️两种场景,在国内绝大多数业务团队中极其罕见。大多数国内团队做的是中后台管理系统、营销活动页、电商交易流程——这些场景的数据结构是相对固定的,前后端的数据契约是稳定的,REST 的简单直接才是最优解。


国内团队的真实最优解:REST + BFF

经过大量团队的试错,国内前端圈逐渐收敛到了一个务实的方案:

前端不直接对接后端微服务,而是通过一个 BFF(Backend For Frontend)层做数据聚合和裁剪。

用户浏览器 → BFF(前端团队维护) → 后端微服务 A / B / C

BFF 由前端团队用 Node.js 或 Cloudflare Workers 维护。它的职责极其明确:

BFF架构数据聚合流程图.png

这个方案用 REST 的简单性,实现了 GraphQL 最核心的两个卖点(按需取数据、数据聚合),同时完全不触碰后端的现有架构,不需要后端团队做任何改造。

它不优雅,但它很管用😃。


怎么做出选择

GraphQL 的理论上限极高,但它的落地成本——缓存体系重建、N+1 治理、后端团队改造、工具链学习——对大多数国内团队来说,远远超出了它带来的收益🤔。

选技术不是选信仰。所以不要因为一个技术的 PPT 很漂亮,就忽略了它的落地代价。真正成熟的技术判断力,是在充分理解一个技术的优势之后,依然能冷静地问出那个最关键的问题:

在我们这个团队、这个业务、这个阶段,这个代价我们付得起吗?

谢谢大家.gif


喜欢我的文章,也欢迎关注我的微信公众号:【前端技术官】。

主要分享:前端架构 · AI 编程 · 职场认知 · 开发者成长

前端技术官-ErpanOmer

微信扫码关注 👆

不定期更新,不刷屏,聊点真正有用的干货。

来源:掘金 · juejin.cn