首页
/ cal.diy(cal.com)中的服务端性能实践:用 React.cache() 做请求级去重

cal.diy(cal.com)中的服务端性能实践:用 React.cache() 做请求级去重

2026-09-07 17:58:33作者:谭伦延

在 Next.js / React Server Components 应用中,同一个页面请求内往往会有多个组件分别发起相同的认证查询或数据库读取,造成重复的 DB 访问。本文基于 cal.diy 仓库内置的 server-cache-react 规则文档(Vercel 出品、MIT 许可的 React 性能规则库中的一条 MEDIUM 级规则),讲解 React.cache() 的请求级去重机制、适用边界,并结合 cal.com 源码中真实的 unstable_cache 封装实现,说明“请求内去重”与“跨请求缓存”在工程上如何各取所需。

一、规则出处:Vercel React Best Practices 规则库中的位置

cal.diy 仓库在 agents/skills/vercel-react-best-practices 中内置了一份由 Vercel Engineering 维护的 React / Next.js 性能优化规则集,共 45 条规则、8 个分类,按影响面排序(见 SKILL.md 中的优先级表):

优先级 分类 影响 前缀
1 消除瀑布流 CRITICAL async-
2 打包体积优化 CRITICAL bundle-
3 服务端性能 HIGH server-
4 客户端数据获取 MEDIUM-HIGH client-
5~8 重渲染 / 渲染 / JS / 高级模式 MEDIUM ~ LOW rerender-

server-cache-react 属于第 3 类“Server-Side Performance(服务端性能)”,规则文件自身元信息(frontmatter)标注为:

  • 标题:Per-Request Deduplication with React.cache()
  • 影响等级:MEDIUM(impactDescription: deduplicates within request
  • 标签server, cache, react-cache, deduplication

完整的规则汇编版见 AGENTS.md 的 3.4 节(约 L715-L735),与本规则文件内容一致;它的姊妹规则 server-cache-lru.md 则解决“跨请求缓存”问题。两条规则合起来覆盖了服务端数据获取的两种去重/缓存场景。

二、规则要解决的问题:同一请求内的重复查询

规则文档给出的核心结论是一句话:React.cache() 做服务端请求级去重,收益最大的是认证(authentication)和数据库查询(database queries)这类场景。

典型痛点是:一个 RSC 页面中,页面组件、侧边栏、导航栏各自独立地调用“获取当前用户”逻辑,若该逻辑内部要 await auth() 拿会话再查一次数据库,那么同一个 HTTP 请求内就会打出多次完全相同的查询。React.cache() 的作用是把这类函数在“单次请求的生命周期内”记忆化:

import { cache } from 'react'

export const getCurrentUser = cache(async () => {
  const session = await auth()
  if (!session?.user?.id) return null
  return await db.user.findUnique({
    where: { id: session.user.id }
  })
})

如规则原文所述:在单个请求内,多次调用 getCurrentUser() 只会真正执行一次查询。 第二个及之后的调用直接复用第一次的结果(对异步函数而言,React 缓存的是返回的 Promise,同一请求内并发调用共享同一个 Promise),从而把 N 次会话解析 + N 次 findUnique 收敛为 1 次。

几个值得注意的机制细节:

  1. 它是“记忆化”而不是“缓存”React.cache() 不引入任何过期时间、容量上限或持久化,记忆化作用域就是一次渲染/一次请求,请求结束即失效。因此它不承担“加速跨请求访问”的职责——这正是需要 LRU 的原因(见第四节)。
  2. 适合无参或纯参函数:规则示例中的 getCurrentUser 不接收参数。记忆化以参数身份为键,参数来自不可变来源(如当前请求内的稳定值)时效果最确定;带复杂可变参数的函数需谨慎评估。
  3. 对认证链路的意义auth() 这类函数通常内部包含 token 校验、会话存储读取甚至数据库查询,是页面中扇出最广的公共依赖,天然适合包一层 cache()

三、边界:请求内去重 vs 跨请求缓存

同属 server- 分类的 server-cache-lru.md 明确划出了 React.cache() 的边界:

React.cache() only works within one request. For data shared across sequential requests(用户先点了按钮 A、又点了按钮 B,两个连续端点需要同一份数据),use an LRU cache.

该规则给出的 LRU 实现范式(影响等级 HIGH,impactDescription: caches across requests):

import { LRUCache } from 'lru-cache'

const cache = new LRUCache<string, any>({
  max: 1000,
  ttl: 5 * 60 * 1000  // 5 minutes
})

export async function getUser(id: string) {
  const cached = cache.get(id)
  if (cached) return cached

  const user = await db.user.findUnique({ where: { id } })
  cache.set(id, user)
  return user
}

// Request 1: DB query, result cached
// Request 2: cache hit, no DB query

适用条件是“用户在数秒内的连续操作会命中多个端点、且这些端点需要同一份数据”。规则同时指出了部署形态对 LRU 有效性的影响:在函数实例可被并发请求共享的托管运行时中,进程内 LRU 天然跨请求生效;而在传统 serverless 中每次调用相互隔离,跨进程缓存则需要 Redis 一类的共享存储。

由此可以整理出清晰的选型表:

维度 React.cache() LRU(进程内/共享存储)
作用域 单次请求 跨请求(受 TTL / 容量约束)
过期机制 无(请求结束即失效) TTL + 容量上限
典型场景 页面内多组件重复取数、认证解析 连续操作命中多端点的同一数据
部署约束 实例隔离时需外置存储

四、cal.com 源码中的对照实践:unstable_cache 封装

从源码结构看,cal.diy 的 Web 应用(Next.js)并没有直接依赖 React.cache(),而是把“请求去重/缓存”建立在 Next.js 的 unstable_cache 之上,并做了仓库级的统一封装。这条实践链可以作为规则落地方式的真实参照。

4.1 仓库级封装:packages/lib/unstable_cache

packages/lib/unstable_cache/unstable_cache.tsnext/cacheunstable_cache 做了一层包装,核心是解决序列化问题(unstable_cache 要求函数返回值可 JSON 序列化):

/**
 * This implementation is adapted from https://github.com/vercel/next.js/issues/51613#issuecomment-1892644565.
 * It is a wrapper around `unstable_cache` that adds serialization and deserialization
 */
import { unstable_cache } from "next/cache";
import { parse, stringify } from "superjson";

export const cache = <T, P extends unknown[]>(
  fn: (...params: P) => Promise<T>,
  keys: Parameters<typeof unstable_cache>[1],
  opts: Parameters<typeof unstable_cache>[2]
) => {
  const wrap = async (params: unknown[]): Promise<string> => {
    const result = await fn(...(params as P));
    return stringify(result);
  };

  const cachedFn = unstable_cache(wrap, keys, opts);

  return async (...params: P): Promise<T> => {
    const result = await cachedFn(params);
    return parse(result);
  };
};

可以看到封装的要点:入参以数组形式收集(unstable_cache 的键机制要求参数可序列化,且多个参数需打包成单一数组参数传入),出参用 superjsonstringify/parse 往返,从而支持 Date 等原生类型。仓库内还有 getInstallCountPerApp.ts 等使用该封装的业务缓存函数。

4.2 业务示例:团队计划校验的缓存与失效

apps/web/app/cache/membership.ts 展示了完整的“缓存 + 标签失效”闭环:

"use server";

import { MembershipRepository } from "@calcom/features/membership/repositories/MembershipRepository";
import { NEXTJS_CACHE_TTL } from "@calcom/lib/constants";
import { revalidateTag, unstable_cache } from "next/cache";

const CACHE_TAGS = {
  HAS_TEAM_PLAN: "MembershipRepository.hasAnyAcceptedMembershipByUserId",
} as const;

export const getCachedHasTeamPlan = unstable_cache(
  async (userId: number) => {
    const hasTeamPlan = await MembershipRepository.hasAnyAcceptedMembershipByUserId(userId);
    return { hasTeamPlan: !!hasTeamPlan };
  },
  ["getCachedHasTeamPlan"],
  {
    revalidate: NEXTJS_CACHE_TTL,
    tags: [CACHE_TAGS.HAS_TEAM_PLAN],
  }
);

export const revalidateHasTeamPlan = async () => {
  revalidateTag(CACHE_TAGS.HAS_TEAM_PLAN, "max");
};

配套的参数与取值(均有仓库依据):

  • revalidate:取自常量 NEXTJS_CACHE_TTL,在 packages/lib/constants.ts 中定义为 3600(秒,即 1 小时)。
  • tags:缓存标签使用“仓库名 + 方法名”命名(如 MembershipRepository.hasAnyAcceptedMembershipByUserId),便于定位缓存归属。
  • 失效方式revalidateTag(tag, "max") 将缓存 TTL 直接推进到最大,即立刻失效——“读多写少 + 变更时定向失效”的组合,而不是无差别清缓存。

4.3 与规则的关系

从源码结构看,cal.com 的选择与规则文档并不矛盾,而是分层互补:

  • React.cache()(规则 server-cache-react)解决的是同一请求内的重复调用,是 React 运行时的记忆化;
  • unstable_cache + 封装 + revalidateTag(cal.com 现状)解决的是跨请求的重复数据获取,缓存键含函数键与参数(userId),带 TTL 与定向失效。

也就是说,若一个页面在同一请求内多处需要 hasAnyAcceptedMembershipByUserId 的结果,unstable_cache 在请求内同样只查一次库(第二次命中缓存);而跨请求的重复读取则完全由 revalidate TTL 覆盖。这正是“请求级去重”与“跨请求缓存”两种手段在真实大型 Next.js 应用中如何协同的例证。

五、落地建议小结

结合规则文档与仓库实践,可以给出可操作清单:

  1. 优先包装高频公共依赖:会话/认证解析、当前用户查询、组织/团队读取等扇出最广的函数,是 React.cache() 的第一梯队候选(规则原文:认证与数据库查询受益最大)。
  2. 认清作用域:它只对单次请求内的多次调用去重,请求之间零共享;不要把业务正确性建立在“它缓存了结果”上。
  3. 异步函数放心用cache() 包裹 async 函数时,同一请求内的并发调用共享同一个 Promise,不会各自开查询。
  4. 跨请求需求另选方案:连续操作命中多端点、且数据在数秒内可复用,用带 TTL 与容量上限的 LRU(见 server-cache-lru.md);Next.js 应用也可直接使用 unstable_cache + revalidateTag 的仓库式封装(参照 unstable_cache 封装membership 缓存)。
  5. 缓存键与失效策略要显式:无论哪种缓存,都建议像 cal.com 那样为每个缓存定义命名标签,并在数据变更点(如成员关系变化)调用定向失效,避免依赖 TTL 自然过期造成的数据陈旧窗口。

参考路径

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
898
5.82 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
921
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.8 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
391