在 Next.js 中分发 React Compiler 产物:react-server 条件导出与双包边界解析指南
导读
当你的第三方 React 组件库开启 React Compiler(下称“编译器”)自动记忆化后,产物会依赖 react/compiler-runtime,这类代码无法安全地在 React Server Component(RSC)环境中直接执行。本篇文章以 Next.js 仓库中 react-compiler 端到端测试的 reference-library 夹具库为核心,剖析这类库应如何通过 exports 的 react-server / default 条件导出,将“已编译客户端产物”与“未编译服务端产物”正确分流,并说明缺少服务端入口时会发生怎样的运行时错误。读完你将掌握 React Compiler 编译产物的解析规律、双包边界的设计方法以及对应的 E2E 验证思路。
这个 README 描述的是什么
本文讲解的主文档位于 test/e2e/react-compiler/reference-library/compiled/README.md,它是 Next.js 自测夹具库 reference-library 的一部分。该目录用于 react-compiler 端到端测试(react-compiler.test.ts),用来验证:
- 在开启
reactCompiler: true的 Next.js 应用中,从 RSC 与客户端两侧导入一个「已发布 React Compiler 编译产物」的第三方库时,模块解析是否会正确命中条件导出; - 库作者漏写
react-server入口时,是否会导致编译器产物被错误地送入 RSC 环境并崩溃。
README 明确指出,compiled/ 下的文件是手动编译的:从 src/index.js 出发,借助 React Compiler Playground 编译记忆化逻辑,再用 Babel Repl 仅编译 JSX runtime。也就是说,这些产物是刻意作为“仓库内 fixture”提交的,用于模拟一个真实的、已发布编译产物的第三方包,而不是由 Next.js 在构建期对 node_modules 重新编译的。
原型源码与三份编译产物的对照
夹具库的源码原型在 src/index.js,内容极简:
export function Container({ children }) {
return <p children={children} />
}
围绕这一原型,compiled/ 目录手工准备了四份产物(见 README 表格对应的实体文件):client.js、index.js、index.react-server.js、missing-react-server.js。
对比 client.js(已编译)与 index.react-server.js(未编译)就能直观看出 React Compiler 做了什么。
未编译版本保持 JSX 直出、无任何缓存逻辑:
import { jsx as _jsx } from 'react/jsx-runtime'
export function Container({ children }) {
return /*#__PURE__*/ _jsx('p', {
children: children,
})
}
已编译版本则引入了记忆化运行时 react/compiler-runtime,把上一次的 props 与子元素存入缓存槽,只有依赖变化时才重新创建元素,实现自动 memo 化:
import { c as _c } from 'react/compiler-runtime'
import { jsx as _jsx } from 'react/jsx-runtime'
export function Container(t0) {
const $ = _c(2)
const { children } = t0
let t1
if ($[0] !== children) {
t1 = /*#__PURE__*/ _jsx('p', {
children: children,
})
$[0] = children
$[1] = t1
} else {
t1 = $[1]
}
return t1
}
_c(n) 是编译器运行时提供的缓存函数,n 表示需要缓存的操作数数量。正是这个 react/compiler-runtime 依赖,把编译产物与普通 JSX 产物区分开来——它是后续所有边界问题的根源。
exports 映射表:四个入口的完整语义
reference-library 的 package.json 通过 exports 精确声明了每个子路径与条件:
{
"name": "reference-library",
"version": "1.0.0",
"types": "./index.d.ts",
"exports": {
"./client": {
"types": "./index.d.ts",
"default": "./compiled/client.js"
},
"./missing-react-server": {
"types": "./index.d.ts",
"default": "./compiled/missing-react-server.js"
},
".": {
"types": "./index.d.ts",
"react-server": "./compiled/index.react-server.js",
"default": "./compiled/index.js"
}
}
}
README 将四份产物的关键属性整理成了一张表,逐列拆解如下:
| 模块标识 | import 条件 | 实际解析文件 | React Compiler | Server-Client 边界 | 适用环境 |
|---|---|---|---|---|---|
reference-library/client |
any | ./compiled/client.js |
是 | 是 | client-only |
reference-library |
react-server |
./compiled/index.react-server.js |
否 | 否 | any |
reference-library |
default | ./compiled/index.js |
是 | 否 | client-only |
reference-library/missing-react-server |
any | ./compiled/missing-react-server.js |
是 | 否 | client-only |
各列含义如下:
- module / import condition:该行描述的是哪个导入路径、以及 Node/打包器环境在哪种条件(condition)下命中;
- resolved:条件命中后实际解析到的文件;
- React Compiler:该产物是否已经过编译器记忆化处理(即是否依赖
react/compiler-runtime); - Server-Client boundary:该文件是否包含
'use client'指令,从而在 RSC 导入时建立服务端—客户端边界; - Valid environment:该产物可被安全运行的 bundle 环境,
client-only意味着它不能被当作服务端代码执行。
之所以 reference-library/client 标记为“边界:是”,是因为它的产物文件 client.js 首行就是 'use client'。任何 RSC 模块导入它都会把 Container 变成一个 Client Reference,组件被延后到客户端渲染,因此即使产物内含编译器缓存代码也能安全运行。
而根入口 . 采用双条件导出:打包 RSC 环境时(Next.js 在服务端 bundle 会激活 react-server 条件)命中未编译的 index.react-server.js;打包客户端时回落到 default 的已编译 index.js。这就是“同一包,两套产物”的经典做法。
为什么 RSC 环境不能直接执行 React Compiler 产物
RSC 环境不执行客户端交互逻辑,也没有 React 渲染调度器可用的完整运行时。React Compiler 编译产物的记忆化要依托 react/compiler-runtime 内部基于当前 dispatcher 的缓存实现,这与 RSC 的服务端渲染模型不匹配。因此 Next.js 的约定是:作为 RSC 解析的服务端入口产物,不应包含 React Compiler 编译产物。
reference-library/missing-react-server 这一行就是为了反证这条约定而设计的:文件 missing-react-server.js 本身是编译过的、且没有 'use client'。当这个子路径从服务端组件导入时,由于缺少可用的服务端/边界入口,编译代码会被直接送入服务端 bundle,最终抛出运行时错误。
破坏性验证:缺少服务端入口的真实错误
E2E 测试中的用例页面 app/library-missing-react-server/page.tsx 显式从服务端组件导入了该子路径:
import { Container } from 'reference-library/missing-react-server'
export const dynamic = 'force-dynamic'
export default function Page() {
return <Container>Library missing react-server</Container>
}
对应测试(见 react-compiler.test.ts)断言在 dev 模式下 CLI 会输出如下错误:
⨯ TypeError: Cannot read properties of undefined (reading 'H')
at Container (**)
测试源码中的 TODO 注释直言该报错信息“不够友好”,理想情况下应提示开发者:该库应当提供一个不使用 React Compiler 的 react-server 入口(参见 react-compiler.test.ts 中关于 NDX-663 的说明)。这说明此类崩溃的本质不是 Next.js 本身缺陷,而是「双包导出缺失」造成的解析事故——编译产物跑错了环境。
E2E 测试如何验证三种导入路径
reference-library 目录整体被 E2E 测试作为 fixture 使用,测试覆盖了三个真实页面路由:
| 路由 | 导入方式 | 预期结果 |
|---|---|---|
/library-react-server |
import { Container } from 'reference-library'(RSC 上下文) |
无报错:命中 react-server 条件解析到未编译产物 |
/library-client |
import { Container } from 'reference-library/client' |
无报错:'use client' 建立边界,编译产物只在客户端执行 |
/library-missing-react-server |
import { Container } from 'reference-library/missing-react-server' |
dev 下抛 TypeError |
对应测试用例分别在 react-compiler.test.ts 中:should work with a library that uses the react-server condition、should work with a library using use client、throws if the React Compiler is used in a React Server environment。
测试还覆盖了三种 React Compiler 集成变体——default、babelrc、rust。其中 rust 变体依赖 Turbopack,并通过 experimental: { turbopackRustReactCompiler: true } 启用(见 react-compiler.test.ts);而 default / babelrc 变体使用的应用级配置就是 test/e2e/react-compiler/next.config.js:
/**
* @type {import('next').NextConfig}
*/
const nextConfig = {
reactCompiler: true,
reactProductionProfiling: true,
}
module.exports = nextConfig
reactCompiler: true 即打开 React Compiler 实验能力,让 Next.js 对应用自身的 Client Components 做自动记忆化。此外测试还为 React 18 场景额外安装 react-compiler-runtime(React 19 起编译器运行时随 React 内置),并安装实验版 babel-plugin-react-compiler(见 react-compiler.test.ts)。
夹具库通过依赖注入方式接入被测应用:非部署场景使用 link:./reference-library,而部署场景因 npm 版本不兼容 link 改用 file:./reference-library(见 react-compiler.test.ts),侧面印证了条件导出解析与真实安装形态强相关。
给库作者与开发者的实践建议
综合这份 README 与配套源码,可以归纳出以下可直接落地的规则:
- 双入口是标配:凡是发布 React Compiler 编译产物的组件库,根入口必须同时声明
react-server与default两个条件,服务端入口指向未编译的源码/中间产物,客户端默认入口才放编译产物。 - 用
'use client'显式声明客户端边界:像./client子路径那样把客户端专用 API 单独导出,并加'use client'指令,RSC 引用时才能安全建立边界。 - 不要漏掉
missing-react-server这类反例:一旦缺少服务端入口且产物又未标记边界,报错会发生在运行期且信息晦涩(如reading 'H')。库作者应通过类似本仓库的 E2E 矩阵(dev / server、webpack / turbopack、三种编译器集成方式)提前暴露解析错误。 - types 也要随入口补齐:
index.d.ts中通过三个declare module分别声明了根包、/client与/missing-react-server的类型签名(见 index.d.ts),确保 TS 用户在任意导入路径下都能获得类型提示,这也与exports中每项都带"types"的写法一致。
最终回到主文档那张只有四行的表——它精准概括了 Next.js 生态中“React Compiler 产物 × 条件导出 × 运行环境”三者间的全部排列组合:有边界的编译产物只能在客户端运行;无边界的编译产物一旦进入 RSC 就会崩溃;唯一能通吃所有环境的是未编译的服务端入口。理解并遵循这一矩阵,是第三方库在开启 React Compiler 的 Next.js 应用中安全分发的前提。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0631
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
video-shotcraftAI宣传片skill,使用 Remotion 制作电影级产品视频:提供106 张镜头配方卡和可复用的视频魔板。适用于 Claude Code 与 Codex以及所有其他智能体Markdown00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python09
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00