首页
/ 在 Next.js 中分发 React Compiler 产物:react-server 条件导出与双包边界解析指南

在 Next.js 中分发 React Compiler 产物:react-server 条件导出与双包边界解析指南

2026-09-07 15:49:25作者:魏献源Searcher

导读

当你的第三方 React 组件库开启 React Compiler(下称“编译器”)自动记忆化后,产物会依赖 react/compiler-runtime,这类代码无法安全地在 React Server Component(RSC)环境中直接执行。本篇文章以 Next.js 仓库中 react-compiler 端到端测试的 reference-library 夹具库为核心,剖析这类库应如何通过 exportsreact-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.jsindex.jsindex.react-server.jsmissing-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-librarypackage.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 conditionshould work with a library using use clientthrows if the React Compiler is used in a React Server environment

测试还覆盖了三种 React Compiler 集成变体——defaultbabelrcrust。其中 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 与配套源码,可以归纳出以下可直接落地的规则:

  1. 双入口是标配:凡是发布 React Compiler 编译产物的组件库,根入口必须同时声明 react-serverdefault 两个条件,服务端入口指向未编译的源码/中间产物,客户端默认入口才放编译产物。
  2. 'use client' 显式声明客户端边界:像 ./client 子路径那样把客户端专用 API 单独导出,并加 'use client' 指令,RSC 引用时才能安全建立边界。
  3. 不要漏掉 missing-react-server 这类反例:一旦缺少服务端入口且产物又未标记边界,报错会发生在运行期且信息晦涩(如 reading 'H')。库作者应通过类似本仓库的 E2E 矩阵(dev / server、webpack / turbopack、三种编译器集成方式)提前暴露解析错误。
  4. 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 应用中安全分发的前提。

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

项目优选

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