rari 深度拆解:当 React 遇上 Rust 运行时——67 倍吞吐量背后的架构哲学与工程实战
2026 年 2 月,rari 登上 Hacker News 首页。一台不到 500MB 内存的服务器,在不扩缩容的情况下扛住了数千并发用户的流量洪峰。这个 React Server Components 框架用 Rust 运行时替代 Node.js,实现了 67.4 倍于 Next.js 的吞吐量和 18.1 倍的响应速度提升。本文从第一性原理深度拆解 rari 的三层架构、V8 嵌入式运行时设计、预压缩响应缓存、流式 SSR 引擎,附完整代码实战与性能调优方法论。
一、为什么 React 需要一个新的运行时?
1.1 Node.js 的天花板
React 生态在过去五年经历了一场静默的范式转移。从 CSR(Client-Side Rendering)到 SSR(Server-Side Rendering),再到 RSC(React Server Components),React 的渲染模型不断向服务端迁移。然而,承载这一切的运行时——Node.js——却始终停留在单线程事件循环的架构上。
让我们用数据说话。以 Next.js 15 为例,在一个包含 8 个组件(1 个 Client Component + 7 个 Server Component)的标准首页上:
| 指标 | Next.js 15 | 瓶颈所在 |
|---|---|---|
| 单请求平均响应时间 | 2.17ms | V8 执行 + Node 事件循环调度 |
| 50 并发吞吐量 | 1,452 req/sec | 单线程 JS 执行 + 序列化开销 |
| P95 延迟 | 43.41ms | GC 暂停 + Node 中间层路由 |
| 构建产物体积 | 634 KB | Node 运行时 + Next.js 框架开销 |
| 构建时间 | 4.42s | Webpack/Turbopack 编译 |
问题的根源不在 V8 引擎本身——V8 的 JIT 编译效率已经相当高。问题在于 Node.js 在 HTTP 服务和 React 渲染之间插入了太多层中间抽象:
客户端请求 → Node.js HTTP Server → Next.js 路由层 → React SSR 渲染器
→ V8 执行组件树 → HTML 序列化 → Node.js HTTP Response → 客户端
每一层都有开销:HTTP 解析、路由匹配、上下文切换、GC 压力、HTML 序列化、压缩……当这些开销在高并发下叠加,吞吐量就崩了。
1.2 Rust 运行时的机会
rari 的核心洞察是:React 的渲染结果本质上是确定性的。给定相同的 props 和数据,Server Component 的输出是固定的。这意味着我们可以:
- 用 Rust 替代 Node.js 的 HTTP 层——axum 提供零拷贝、多线程的 HTTP 服务
- 用嵌入式 V8 替代 Node.js 运行时——deno_core 提供无 Node 开销的 V8 绑定
- 用预压缩缓存消除重复渲染——第一次请求渲染,后续请求直接从内存返回压缩字节
这不是简单的"用 Rust 重写 Node.js",而是一次架构层面的重新设计。
二、rari 的三层架构
rari 的名字是 Runtime Accelerated Rendering Infrastructure 的缩写。它的架构分为三层,每层各司其职:
┌─────────────────────────────────────────┐
│ Layer 3: Build Toolchain │
│ Rolldown + Vite + TypeScript 7 │
├─────────────────────────────────────────┤
│ Layer 2: React Framework │
│ App Router / RSC / Streaming / Suspense│
├─────────────────────────────────────────┤
│ Layer 1: Rust Runtime │
│ axum HTTP + deno_core V8 + DashMap │
└─────────────────────────────────────────┘
2.1 Layer 1:Rust 运行时——HTTP 与 V8 的双引擎
这是 rari 与所有其他 React 框架最本质的区别。运行时由两个独立运行的引擎组成:
HTTP 引擎:基于 axum(Tokio 生态的 HTTP 框架),运行在多线程 tokio 运行时上。每个 OS 线程独立处理请求,无锁设计避免了线程间竞争。
JS 引擎:基于 deno_core::JsRuntime(Deno 的 V8 绑定),运行在专用的单线程 tokio 运行时上。这个线程拥有完整的 V8 事件循环,可以处理 Promise 和 async 组件渲染。
两个引擎通过一个 消息通道(mpsc channel) 通信:
// 简化的架构示意
let (tx, rx) = tokio::sync::mpsc::channel::<RenderRequest>(64);
// HTTP 线程池(多线程)
let http_handle = tokio::spawn(async move {
let app = Router::new()
.route("/*path", get(handle_request));
axum::serve(listener, app).await;
});
// JS 线程(单线程)
let js_handle = tokio::spawn(async move {
let mut runtime = deno_core::JsRuntime::new(/* ... */);
while let Some(req) = rx.recv().await {
let result = runtime.execute_script(&req.composition).await;
req.tx.send(result).await;
}
});
关键设计决策:消息通道只在缓存未命中时使用。一旦某个路由被渲染并缓存,HTTP 线程直接从内存返回响应,完全不需要唤醒 V8。这意味着在热缓存状态下,V8 可以完全空闲,吞吐量只受 HTTP 层限制。
2.2 Layer 2:React 框架——RSC 的完整语义
rari 实现了 React Server Components 的完整语义,包括:
- App Router:基于文件系统的路由,支持 layouts、loading states、error boundaries
- Server Components:默认的服务端组件,
'use client'显式声明客户端组件 - Server Actions:服务端数据变更,通过
'use server'指令声明 - Streaming SSR:基于 Suspense 边界的渐进式渲染
- TypeScript 7:跨服务端/客户端边界的完整类型安全
2.3 Layer 3:构建工具链——Rolldown + Vite
rari 使用 Rolldown 驱动的 Vite 作为构建工具。Rolldown 是 Rust 编写的 JavaScript 打包器,速度远超传统的 Webpack/esbuild。
# 创建项目
pnpm create rari-app@latest my-app
cd my-app
# 开发(热重载)
pnpm dev
# 生产构建
pnpm build
# 启动(Rust 二进制 + 嵌入式 V8)
pnpm start
构建时间对比:rari 1.75s vs Next.js 4.42s(快 2.5 倍)。
三、预压缩响应缓存——67 倍性能的秘密
rari 的性能优势中,最大的一块来自 预压缩响应缓存(Pre-compressed Response Cache)。让我们逐层拆解这个机制。
3.1 请求的完整生命周期
请求到达 → HTTP 线程检查缓存 → 缓存命中?
├── 是 → 从 DashMap 读取预压缩字节 → 直接写入 socket → 完成
└── 否 → 发送渲染请求到 V8 线程 → V8 渲染组件树
→ 生成 RSC Flight 协议 → 转换为 HTML
→ zstd 压缩 → 存入 DashMap → 返回压缩响应
3.2 两级缓存结构
rari 使用两级缓存来最大化命中率:
第一级:渲染缓存(Render Cache)
- Key:路由 + 上下文的哈希值
- Value:渲染后的 HTML 字符串
- 用途:避免重复执行组件树渲染
第二级:响应缓存(Response Cache)
- Key:路由 + Accept-Encoding 的哈希值
- Value:预压缩的响应字节(zstd/brotli/gzip 三份)
- 用途:避免重复压缩
两级缓存都存储在 DashMap 中——这是一个高性能的并发 HashMap,支持无锁读取。
// 缓存数据结构(简化)
struct CacheEntry {
raw_html: Bytes, // 未压缩的 HTML
zstd_compressed: Bytes, // zstd 压缩
brotli_compressed: Bytes, // brotli 压缩
gzip_compressed: Bytes, // gzip 压缩
etag: String, // 缓存验证
headers: HeaderMap, // 响应头
}
// DashMap 并发缓存
type ResponseCache = DashMap<CacheKey, CacheEntry>;
3.3 为什么 67 倍?
让我们做一个公平的对比。两个框架都使用各自的缓存机制:
| 组件 | rari | Next.js |
|---|---|---|
| 缓存存储 | DashMap(内存,无锁) | Node.js Route Cache |
| 缓存读取 | Rust 哈希查找 + memcpy | Node.js 序列化/反序列化 |
| 响应发送 | Rust HTTP 线程直接写 socket | Node.js HTTP Response 管道 |
| 压缩 | 预计算,零开销 | 每次请求实时压缩(或 CDN) |
| V8 参与 | 缓存命中时不参与 | 缓存命中时仍需经过 Node 层 |
差距的核心在于:rari 的缓存命中路径是 hash lookup + memcpy,而 Next.js 即使缓存命中也要经过 Node.js 的 HTTP 处理管线。67 倍不是 Rust 比 JavaScript 快 67 倍,而是架构设计消除了不必要的中间层。
3.4 缓存失效策略
预压缩缓存听起来简单,但缓存失效是关键难题。rari 提供了三种失效机制:
1. HMR 失效:开发模式下,模块热更新时自动清除受影响的缓存条目。
2. revalidatePath 失效:Server Action 中调用 revalidatePath('/some-route') 时,精确清除指定路由的缓存。
3. Server Action 失效:任何 'use server' 函数执行后,自动清除所有缓存(保守策略)。
// app/actions.ts
'use server'
import { revalidatePath } from 'rari/cache'
export async function updatePost(id: string, data: FormData) {
await db.post.update({ where: { id }, data: Object.fromEntries(data) })
// 精确失效:只清除受影响的路由
revalidatePath(`/posts/${id}`)
revalidatePath('/posts')
}
四、流式 SSR——在 Rust 层实现 React Streaming
当页面包含 Suspense 边界(通过 loading.tsx 声明)时,rari 从缓存模式切换到流式渲染模式。这是 rari 最复杂的子系统。
4.1 流式渲染的完整流程
1. StreamingRenderer 在 V8 中执行组合脚本
2. V8 返回:初始 RSC 树 + Suspense 边界列表 + 待解析 Promise 列表
3. 初始树转换为 HTML,作为第一个 chunk 发送
4. 后台 Promise 解析器批量发送 pending promises 到 V8
5. Promise 解析完成后,发送边界更新(带 DOM 位置提示)
6. 浏览器通过内联 <script> 将更新插入正确位置
7. 所有 Promise 完成后,关闭流,嵌入 RSC payload 用于 hydration
4.2 Rust 层的流式实现
rari 的流式 SSR 在 Rust 层而非 Node.js 层实现,这是性能优势的另一个来源:
// 流式渲染器(简化)
async fn stream_render(
composition: CompositionScript,
v8: &mut JsRuntime,
response: &mut Response,
) {
// 第一步:V8 渲染初始树
let (initial_tree, suspense_boundaries, pending_promises) =
v8.render_composition(composition).await;
// 第二步:发送 HTML shell
let shell = render_to_html(initial_tree);
response.send_chunk(shell).await;
// 第三步:后台 Promise 解析
let tx = response.chunk_sender();
let pending = pending_promises.clone();
tokio::spawn(async move {
// 批量发送 pending promises 到 V8
let resolved = v8.resolve_batch(pending).await;
for (boundary_id, resolved_content) in resolved {
// 去重 + DOM 位置提示
let update = render_suspense_update(boundary_id, resolved_content);
tx.send(update).await;
}
});
// 第四步:关闭流,嵌入 hydration payload
let rsc_payload = v8.get_rsc_payload();
response.send_final_chunk(render_hydration_script(rsc_payload)).await;
response.close().await;
}
4.3 $RC 模式——React 的秘密武器
rari 使用 React 内部的 $RC(Replace Content)模式来更新 Suspense 边界。这个模式通过一个隐藏的 <div> + 内联 <script> 实现无刷新内容替换:
<!-- 流式更新 chunk -->
<div id="B:0" style="display:none"><!-- 内容 --></div>
<script>$RC("B:0", "S:0")</script>
浏览器执行 $RC 脚本时,会将隐藏 div 的内容移动到目标位置,然后移除临时元素。整个过程无需重新渲染整个组件树,只替换变化的部分。
五、V8 嵌入——无 Node.js 的 JavaScript 执行
5.1 为什么不用 Node.js?
rari 选择嵌入 V8 而非使用 Node.js,有三个关键原因:
- 启动时间:Node.js 冷启动需要加载大量内置模块,deno_core 的 V8 嵌入可以在毫秒级完成
- 内存开销:Node.js 常驻内存通常在 50-100MB,嵌入式 V8 只需要 V8 本身的核心内存
- 无事件循环竞争:Node.js 的事件循环是全局的,HTTP 处理和 JS 执行共享同一个循环;rari 的 V8 有独立的 tokio 运行时
5.2 Deno Core 的复用
rari 并没有从零实现 V8 绑定,而是复用了 Deno 项目的核心组件:
- deno_core:V8 事件循环、模块加载、Promise 调度
- deno_ops:V8 操作的 Rust 封装
- deno_permissions:权限系统(虽然 rari 不直接暴露给用户)
use deno_core::{JsRuntime, RuntimeOptions};
// 初始化嵌入式 V8
let mut runtime = JsRuntime::new(RuntimeOptions {
module_loader: Some(Box::new(NodeModuleLoader)),
extensions: vec![
// Deno 的标准 API:fetch, fs, crypto 等
deno_fetch::init_ops_and_esm(/* ... */),
deno_fs::init_ops_and_esm(/* ... */),
deno_net::init_ops_and_esm(/* ... */),
],
// 启用快照加速启动
startup_snapshot: Some(SNAPSHOT_BIN),
..Default::default()
});
// 执行 React 组件渲染脚本
let result = runtime.execute_script(
"render_composition",
&composition_script,
).await?;
5.3 node_modules 支持——务实的选择
与 Deno 和 Bun 不同,rari 选择直接支持 node_modules。这是一个非常务实的决策:
// rari 中直接使用 npm 包
import { marked } from 'marked' // ✅ 直接从 node_modules 解析
import express from 'express' // ✅ 兼容现有生态
import { PrismaClient } from '@prisma/client' // ✅ 无需 import map
rari 在运行时解析 node_modules,但结果会被缓存。大部分服务端代码在构建阶段已被 Vite 预打包,所以运行时解析的开销主要发生在首次请求。
六、实战:从零构建一个 rari 应用
6.1 项目初始化
# 使用官方脚手架
pnpm create rari-app@latest my-rari-app
cd my-rari-app
# 目录结构
my-rari-app/
├── app/
│ ├── layout.tsx # 根布局(Server Component)
│ ├── page.tsx # 首页(Server Component)
│ ├── posts/
│ │ ├── page.tsx # 文章列表
│ │ ├── [id]/
│ │ │ └── page.tsx # 文章详情
│ │ └── loading.tsx # 加载状态(触发 Streaming SSR)
│ └── globals.css
├── components/
│ ├── Counter.tsx # Client Component 示例
│ └── PostList.tsx # Server Component 示例
├── lib/
│ └── db.ts # 数据库连接
├── app/actions.ts # Server Actions
├── vite.config.ts
├── package.json
└── tsconfig.json
6.2 Server Component 实战
// app/page.tsx — 纯 Server Component,零客户端 JS
import { db } from '@/lib/db'
export default async function HomePage() {
// 直接在组件中查询数据库(RSC 的核心优势)
const posts = await db.post.findMany({
orderBy: { createdAt: 'desc' },
take: 10,
include: { author: true },
})
return (
<main className="container mx-auto py-8">
<h1 className="text-4xl font-bold mb-8">最新文章</h1>
<div className="grid gap-6">
{posts.map(post => (
<article key={post.id} className="border rounded-lg p-6">
<h2 className="text-2xl font-semibold">
<a href={`/posts/${post.id}`}>{post.title}</a>
</h2>
<p className="text-gray-600 mt-2">{post.excerpt}</p>
<div className="text-sm text-gray-400 mt-4">
{post.author.name} · {new Date(post.createdAt).toLocaleDateString()}
</div>
</article>
))}
</div>
</main>
)
}
这个组件在服务端执行,不会向客户端发送任何 JavaScript。数据库查询直接在组件中完成,无需 API 层。
6.3 Client Component 实战
// components/Counter.tsx — 需要交互的客户端组件
'use client'
import { useState } from 'react'
export function Counter() {
const [count, setCount] = useState(0)
return (
<div className="flex items-center gap-4">
<button
onClick={() => setCount(c => c - 1)}
className="px-4 py-2 bg-gray-200 rounded"
>
-
</button>
<span className="text-2xl font-mono w-12 text-center">{count}</span>
<button
onClick={() => setCount(c => c + 1)}
className="px-4 py-2 bg-blue-500 text-white rounded"
>
+
</button>
</div>
)
}
6.4 Server Action 实战
// app/actions.ts
'use server'
import { revalidatePath } from 'rari/cache'
import { db } from '@/lib/db'
export async function createPost(formData: FormData) {
const title = formData.get('title') as string
const content = formData.get('content') as string
if (!title || !content) {
throw new Error('Title and content are required')
}
await db.post.create({
data: { title, content, authorId: 'current-user-id' },
})
// 创建后刷新相关路由的缓存
revalidatePath('/')
revalidatePath('/posts')
}
export async function deletePost(id: string) {
await db.post.delete({ where: { id } })
revalidatePath('/')
revalidatePath(`/posts/${id}`)
}
6.5 流式加载体验
// app/posts/loading.tsx — 触发 Streaming SSR
export default function PostsLoading() {
return (
<div className="container mx-auto py-8">
<h1 className="text-4xl font-bold mb-8">最新文章</h1>
{/* 骨架屏:立即显示,无需等待数据加载 */}
<div className="grid gap-6">
{Array.from({ length: 5 }).map((_, i) => (
<div key={i} className="border rounded-lg p-6 animate-pulse">
<div className="h-8 bg-gray-200 rounded w-2/3 mb-4" />
<div className="h-4 bg-gray-200 rounded w-full mb-2" />
<div className="h-4 bg-gray-200 rounded w-4/5" />
</div>
))}
</div>
</div>
)
}
当 loading.tsx 存在时,rari 自动切换到流式渲染:先发送骨架屏 HTML,数据加载完成后通过 Suspense 边界替换为真实内容。
七、性能调优方法论
7.1 缓存命中率最大化
// ✅ 好的做法:静态 Server Component,自动缓存
export default async function StaticPage() {
const data = await fetchData()
return <div>{/* 渲染 */}</div>
}
// ❌ 避免:不需要 loading.tsx 的页面不要加
// 每个 loading.tsx 都会禁用缓存路径,切换到流式渲染
// ✅ 需要动态数据时:使用 loading.tsx + Suspense
export default async function DynamicPage() {
const data = await fetch('https://api.example.com/data')
return <div>{/* 渲染 */}</div>
}
// app/dynamic/loading.tsx — 这个文件的存在告诉 rari 使用流式路径
7.2 减少客户端 JS 体积
// ✅ 默认使用 Server Component
export default async function Page() { /* ... */ }
// ✅ 只在需要交互时标记 'use client'
'use client'
export function InteractiveWidget() { /* ... */ }
// ❌ 避免整个页面标记 'use client'
'use client' // 这会让整个组件树都变成客户端组件
export default function Page() { /* ... */ }
rari 的基准测试显示,同等功能下:
- rari 产物体积:285 KB
- Next.js 产物体积:634 KB
- 减少 55%
7.3 内存优化
rari 的生产服务器内存占用通常在 500MB 以下(包含完整的 PostHog 分析和 Sentry 错误追踪)。对比 Next.js 的 1-2GB 基线,10 个实例可以节省 15GB+ 内存。
# 监控内存使用
curl -s http://localhost:5173/__rari/health | jq '.memory'
7.4 生产部署
# Dockerfile 示例
FROM rust:1.78-slim as builder
WORKDIR /app
COPY . .
RUN cargo build --release
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
COPY --from=builder /app/target/release/my-rari-app /usr/local/bin/
EXPOSE 3000
CMD ["my-rari-app"]
生产环境只需要 Rust 二进制文件 + 嵌入式 V8。Node.js 仅在构建阶段需要(CLI + Vite 打包),运行时完全不依赖。
八、与竞品的深度对比
8.1 rari vs Next.js
| 维度 | rari | Next.js 15 |
|---|---|---|
| 运行时 | Rust + V8 | Node.js |
| 吞吐量 | 97,826 req/sec | 1,452 req/sec |
| 响应时间 | 0.12ms | 2.17ms |
| 产物体积 | 285 KB | 634 KB |
| 构建时间 | 1.75s | 4.42s |
| 内存占用 | <500MB | 1-2GB |
| node_modules | ✅ 直接支持 | ✅ 原生 |
| 生态成熟度 | 早期(v0.15) | 成熟(v15) |
| Edge 部署 | 发展中 | Vercel 原生 |
8.2 rari vs Remix
| 维度 | rari | Remix |
|---|---|---|
| 渲染模型 | RSC(默认服务端) | 传统 Loader/Action |
| 运行时 | Rust + V8 | Node.js / Edge |
| 流式支持 | 原生 Streaming SSR | 有限 |
| 数据获取 | 组件内直接查询 | Loader 函数 |
8.3 rari vs Waku
| 维度 | rari | Waku |
|---|---|---|
| 运行时 | Rust + V8 | Node.js |
| 构建工具 | Rolldown + Vite | Vite |
| 性能 | 67x Next.js | 接近 Next.js |
| 成熟度 | 早期 | 早期 |
九、rari 的局限与未来
9.1 当前的局限
- 生态成熟度:v0.15 阶段,中间件、插件系统尚在发展中
- Edge 部署:尚无 Cloudflare Workers / Vercel Edge 的原生支持
- 调试工具:DevTools 生态不如 Next.js 成熟
- Node.js 依赖:构建阶段仍需要 Node.js(CLI + Vite)
- 冷启动:首次渲染需要 V8 初始化 + 模块加载,比缓存路径慢
9.2 未来方向
rari 的创始人 Ryan Skinner 将在 2026 年 10 月的 React Advanced 大会上发表演讲,主题是 "React Server Components Are a Serialization Format, Not a Framework Feature"。这暗示了 rari 的长期愿景:
- RSC 不应该是某个框架的私有特性,而应该是一种标准化的序列化格式
- 运行时应该可以自由选择(Node.js、Rust、Go、甚至浏览器原生)
- 性能优化应该在基础设施层完成,而非框架层
十、总结
rari 不是又一个"用 Rust 重写一切"的玩具项目。它代表了 React 生态的一个重要方向:将渲染计算从应用层下沉到基础设施层。
从架构上看,rari 的三层设计(Rust Runtime → React Framework → Build Toolchain)实现了关注点分离。HTTP 服务、JS 执行、React 渲染各司其职,通过消息通道协作。
从性能上看,67 倍的吞吐量提升不是来自"Rust 比 JavaScript 快"这种简单叙事,而是来自架构优化:预压缩缓存消除了重复渲染和压缩,V8 独立运行消除了事件循环竞争,Rust HTTP 层消除了 Node.js 中间层。
从工程上看,rari 保持了对现有生态的兼容(node_modules 支持、TypeScript 优先),降低了迁移门槛。开发者不需要学习新的 API,只需要将 next.config.js 换成 vite.config.ts。
如果你的团队在 Next.js 上遇到了性能瓶颈,或者在评估下一代 React 框架,rari 值得认真考虑。它不一定适合所有场景——生态成熟度和 Edge 部署仍然是短板——但它的架构思路和技术方向,值得每个 React 开发者关注。
参考资源:
- rari 官方文档:https://rari.build/docs
- GitHub 仓库:https://github.com/rari-build/rari
- 性能基准测试:https://github.com/rari-build/benchmarks
- Ryan Skinner 博客:https://ryanskinner.com
- axum(Rust HTTP 框架):https://github.com/tokio-rs/axum
- deno_core(V8 嵌入):https://github.com/denoland/deno
- DashMap(并发 HashMap):https://github.com/xacrimon/dashmap