避免服务端共享模块状态:React Server Components 并发渲染下的请求数据隔离最佳实践
AI 应用媒体生成前端AI AgentAI 技能【免费下载链接】infinite-canvas面向 AI 创作的开源无限画布工作台集成 AI 生图、参考图编辑、视频生成、Agent 智能助手、画布编排、对话创作、提示词库与素材管理等能力支持可视化创作流程与多 Agent 协同工作。兼容 OpenAI 接口生态支持 chatgpt2api、grok2api、flow2api、newapi 等渠道接入。项目地址https://gitcode.com/gh_mirrors/infinit/infinite-canvas点击查看免费下载本文围绕开源仓库 infinite-canvas 内置的 Vercel React 最佳实践技能库中server-no-shared-module-state规则展开讲解在 React Server ComponentsRSC与 SSR 场景下为何不能使用模块级可变变量承载请求数据并结合仓库源码docs/文档站的 Next.js App Router 实现给出可落地的代码改造与检查清单。读完本文你将掌握识别请求数据泄漏隐患、正确通过 props 传递请求局部数据以及区分安全例外静态配置、正确命名的共享缓存、进程级单例的完整判断方法。规则定位Server-Side Performance 类别下的 HIGH 影响级规则server-no-shared-module-state.md是 .agents/skills/vercel-react-best-practices 技能库中 70 条规则之一。该技能库由 Vercel 工程团队维护按影响度分为 8 大类本规则属于第 3 类Server-Side PerformanceHIGH命名前缀为server-见 SKILL.md。规则头部声明的元数据frontmatter明确给出了它的定位title: Avoid Shared Module State for Request Dataimpact: HIGHimpactDescription: prevents concurrency bugs and request data leaks防止并发 bug 与请求数据泄漏tags: server, rsc, ssr, concurrency, security, state在编译后的完整指南 AGENTS.md 中它对应第 3.3 节与服务器端认证 Server Actions3.1避免 RSC Props 重复序列化3.2跨请求 LRU 缓存3.4静态 I/O 提升到模块级3.5等规则共同构成服务端性能治理体系。理解这条规则需要同时掌握它的反面规则 server-hoist-static-io.md静态 I/O 恰恰应当提升到模块级——两者看似矛盾实则是一条统一原则的两面模块级只能放对每个请求都相同的东西绝不能放随请求变化的东西。问题本质模块作用域是进程级共享内存不是请求局部状态规则的核心论断只有一句话Treat module scope on the server as process-wide shared memory, not request-local state.即在服务端请把模块作用域视为进程级的共享内存而不是请求局部状态。这是因为 React Server Components 与 SSR 渲染存在两个关键事实同一个进程中可以并发执行多个渲染。在 Node.js 服务进程内多个请求的渲染可以交错执行尤其在使用Promise.all、Suspense、流式渲染时渲染任务会在 await 点让出执行权。模块级变量是所有并发任务共用的。ES Module 的模块作用域在整个进程生命周期内只初始化一次任何一次渲染对它的写入都会立刻被其他并发的渲染读取到。两者叠加就产生了一类非常隐蔽的故障如果某个渲染把请求 A 的数据写进模块级变量另一个渲染在读取它时拿到的可能是请求 B 的数据。这会造成竞态条件race conditions、跨请求数据污染cross-request contamination严重时直接演变为安全漏洞——一个用户的数据出现在另一个用户的响应里如用户身份、订单、会话信息被串台。错误示例请求数据泄漏到并发渲染之间规则给出了一个典型反例把用户身份存进了模块级可变变量let currentUser: User | null null export default async function Page() { currentUser await auth() return Dashboard / } async function Dashboard() { return div{currentUser?.name}/div }其故障推演如下请求 A 到达Page()执行await auth()在等待期间渲染让出执行权请求 B 到达Page()执行await auth()把currentUser覆盖为请求 B 的用户请求 A 的auth()resolvePage()返回Dashboard /Dashboard读取currentUser—— 此时它已经是请求 B 的用户于是请求 A 的响应中渲染出了请求 B 用户的姓名。即使没有并发只要两个请求在时间上重叠这正是生产环境的常态这个 bug 就必然发生而且无法通过单次请求的本地测试复现属于典型的偶发性、难调试、影响安全的问题。同样的隐患也适用于var声明、模块级Map/Set缓存写入以及任何先写后读的模块级可变容器。关键不在于变量声明方式而在于把请求相关的数据挂在了进程级共享位置上。正确做法让请求数据随渲染树流动props 传递规则给出的正确写法是把请求数据保持在渲染树局部export default async function Page() { const user await auth() return Dashboard user{user} / } function Dashboard({ user }: { user: User | null }) { return div{user?.name}/div }这里有两个关键变化currentUser从模块级变量变为Page函数内部的const user—— 每次渲染每个请求都有自己的副本互不干扰Dashboard通过props接收数据而不是从模块作用域读取 —— 数据随渲染树自上而下流动作用域严格限定在当前渲染内。如果数据需要在组件树深层共享可以继续通过 props 逐层传递或用 ContextRSC 中可以传递序列化数据但绝不能回到模块级变量。判断标准很简单凡是每个请求/每个用户都可能不同的数据都必须来自请求参数、数据库查询、认证结果等请求时数据源并在组件/函数局部传递。安全例外清单什么可以合法地放在模块级规则明确给出了三类安全例外。它们是模块级共享状态的合法使用场景但都有严格的边界条件1. 不可变的静态资源与配置加载一次在模块作用域加载一次、之后不再改变的静态资源或配置。这正是 server-hoist-static-io.md 规则鼓励的做法字体文件、Logo、图标、运行时不变的配置文件都应当提升到模块级避免每次请求重复进行文件/网络 I/O。注意边界静态资源必须对每个请求都相同。按请求或用户变化的资源如用户头像、个性化模板、运行期可能变化的文件、过大的内存占用文件、以及不应常驻内存的敏感数据都不属于此类。2. 有意设计为跨请求复用且正确命名的共享缓存比如按 key 组织的缓存其设计目的就是跨请求复用并且缓存键正确地包含请求相关维度如用户 ID、资源 ID。仓库中与之配套的规则是 server-cache-lru.md其示例用LRUCache以id为键缓存用户数据import { LRUCache } from lru-cache const cache new LRUCachestring, 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 }这里的要点是缓存容器本身是共享的但缓存内容必须通过 key 严格区分请求。如果缓存键不包含用户维度就会退化为共享模块状态重新引入数据串台风险。与之互补的还有 server-cache-react.mdReact.cache()是单请求内的去重缓存认证查询、数据库查询在同一渲染树内只执行一次它不跨请求共享因此天然安全。两者的分工是单请求去重用React.cache()跨请求复用才用 LRU。3. 不存储请求/用户可变数据的进程级单例例如连接池、日志器、全局配置对象、无状态的工具函数集等。判断标准是该对象本身不保存任何 request-specific 或 user-specific 的可变数据。一旦单例里出现上次请求的用户这类字段它就不再安全。仓库实战印证docs 文档站源码中的模块级常量与请求局部数据流在 infinite-canvas 仓库中docs/是一个基于 Next.js App Router 构建的 Fumadocs 文档站配置见 docs/next.config.mjs包含reactStrictMode: true与静态导出相关配置。它的源码恰好为上述三类边界提供了真实的对照样本。安全例外的正面样本模块级不可变常量doc-shared.ts 在模块作用域声明了一组纯静态配置属于不可变静态配置的安全例外export const appNames { en: Infinite Canvas, zh-CN: 无限画布, }; export const docsRoute /docs; export const docsContentRoute /llms.mdx/docs;appNames是普通对象字面量本质是不可变配置docsRoute等是常量字符串——它们对所有请求、所有用户完全相同声明在模块级没有任何并发风险。类似地i18n.ts 中的i18n defineI18n(...)也是模块级的只读配置。注意这些模块没有导出任何可变变量let/var——从源码结构看这是刻意为之共享的都是不会变的东西。请求局部数据流的正面样本异步服务端组件与路由处理器文档页面是一个典型的 RSC 服务端组件docs/src/app/[lang]/docs/[...slug]/page.tsxexport default async function Page(props: PageProps/[lang]/docs/[...slug]) { const params await props.params; const page source.getPage(params.slug, params.lang); if (!page) notFound(); return DocPageContent page{page} /; }它的数据流完全遵循请求局部原则params来自请求路由参数page是在函数局部由params推导的结果DocPageContent通过 props 接收page——没有任何数据经过模块级可变状态中转。source本身是 source.ts 中的模块级加载器但它是有状态读取接口的只读访问入口不存储请求数据。再看路由处理器docs/src/app/llms.txt/route.tsexport async function GET(request: Request) { const locale new URL(request.url).searchParams.get(locale) ?? en; const docsIndex await readFile(join(process.cwd(), locale zh-CN ? index.zh-CN.md : index.md), utf8); return new Response([docsIndex, llms(source).index(locale)].join(\n\n)); }locale从请求 URL 解析而来仅存在于GET函数内部llms-full.txt/route.ts 中source.getPages(locale).map(getLLMText)的结果同样只停留在函数作用域内。如果这些请求时数据被提升为模块级let locale两个并发请求就会互相覆盖 locale导致英文用户拿到中文文档——这正是本规则要杜绝的跨请求污染。此外该路由处理器设置了export const revalidate false配合文档站静态生成策略且页面通过generateStaticParams()预生成见上述 page.tsx 与 docs/src/app/[lang]/layout.tsx 的generateStaticParams说明文档站在构建期静态化为主但一旦站点部署为 Node 服务端渲染SSR模式或加入动态路由并发渲染就会真实发生此时任何模块级可变状态都会立刻成为泄漏点。规则的价值正是在静态→动态切换时体现代码评审阶段就该杜绝模块级可变请求状态。相邻规则边界一套完整的服务端状态治理方案本规则并非孤立存在它与同组server-规则共同构成服务端状态治理的完整闭环规则文件解决的问题与共享模块状态的关系server-no-shared-module-state.md请求数据泄漏、并发竞态本文主题请求数据绝不入模块级可变状态server-hoist-static-io.md每次请求重复 I/O反面互补静态数据恰恰要提升到模块级server-cache-react.md单请求内重复查询安全的请求内去重不跨请求共享server-cache-lru.md跨请求重复查询安全例外正确命名的共享缓存server-after-nonblocking.md日志/分析阻塞响应响应后异步执行避免阻塞记忆口诀随请求变的走 props对请求不变的留模块要缓存就必须正确命名要共享就不能带用户状态。落地检查清单在编写或评审 RSC / SSR 代码时逐条核对以下问题是否存在模块级let/var可变变量如果有检查它是否被请求数据用户、会话、参数、查询结果赋值。请求数据是否在函数内部声明、随 props 传递任何写入模块→子组件读取的模式都必须改写。模块级常量的值是否对所有请求恒定若是auth()结果、用户查询结果等请求派生数据即使当前恰好只写一次也必须下放到请求局部。缓存是否按请求维度正确命名缓存值如果可能因用户/请求而异key 必须包含对应维度否则视为共享状态处理。单例对象是否保存可变请求数据连接池、日志器等进程级单例中不应出现最近一次请求之类的字段。静态 I/O 是否已提升到模块级与 server-hoist-static-io.md 对照执行避免把静态资源误放在请求内重复加载。最后回到本文开头那句最重要的定位模块作用域是进程级共享内存不是请求局部状态。把它作为服务端代码的第一性原理就能同时避开并发竞态、跨请求污染与用户数据泄漏这三类高影响缺陷。赞分享AI 应用媒体生成前端AI AgentAI 技能【免费下载链接】infinite-canvas面向 AI 创作的开源无限画布工作台集成 AI 生图、参考图编辑、视频生成、Agent 智能助手、画布编排、对话创作、提示词库与素材管理等能力支持可视化创作流程与多 Agent 协同工作。兼容 OpenAI 接口生态支持 chatgpt2api、grok2api、flow2api、newapi 等渠道接入。项目地址https://gitcode.com/gh_mirrors/infinit/infinite-canvas点击查看免费下载相关推荐preguntas-entrevista-react 工程实践React 服务端并发渲染下的共享模块状态陷阱与请求数据隔离preguntas entrevista react 工程实践React 服务端并发渲染下的共享模块状态陷阱与请求数据隔离 本文围绕 React 服务端组件前端教程jetbrains-cc-gui 内 Vercel React 服务端最佳实践避免共享模块状态引发请求数据泄漏与并发竞态jetbrains cc gui 内 Vercel React 服务端最佳实践避免共享模块状态引发请求数据泄漏与并发竞态 导读 本文讲解 jetbrains在 RSC 与 SSR 中杜绝模块级共享状态open-slide 服务端并发渲染的请求数据隔离实战在 RSC 与 SSR 中杜绝模块级共享状态open slide 服务端并发渲染的请求数据隔离实战 本篇技术指南聚焦 Vercel Engineering 维上一篇Twenty Syncable Entity 集成测试实战基于 Query Factory Wrapper 双文件模式的 Metadata CRUD 测试套件下一篇YOLOX C高性能部署指南TensorRT与ncnn推理引擎对比实战创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考