Remix 3 深度拆解:当全栈框架决定「叛逃」React——从中心栈到 Web 原生、Preact 分叉与 unbundling 范式的一次悬崖勒马
2026年8月4日,Remix 团队发布了一个让整个前端社区炸锅的 beta 版本:Remix 3.0.0-beta.5。这不是一次普通的版本迭代,而是一次从底层连根拔起的彻底重写——框架放弃了经营多年的 React 生态,迁移到 Preact 分支,拥抱 Web 平台原生能力,甚至引入了"unbundling"(取消打包)的激进理念。
消息一出,Hacker News 单帖评论数破千,r/reactjs 上有开发者写道:"Remix 现在已经彻底变成另一个东西了,完全看不出以前那个 Remix 的影子。"
这是一篇技术深度拆解。我们不聊情怀,只拆架构:Remix 3 到底改了什么?为什么改?代价是什么?它代表的前端框架演进方向,你该不该跟?
一、背景:从「React 最好的全栈框架」到「Web 标准的信徒」
1.1 Remix 的历史定位
Remix 诞生于 2020 年,由 React Router 团队打造,定位是"React 生态中最懂 Web 平台的全栈框架"。它的核心卖点是:
- 嵌套路由(Nested Routes):让数据加载和布局复用天然契合
- 渐进增强(Progressive Enhancement):HTML 表单天然可用,JavaScript 只是增强层
- 错误边界与加载状态:用
<Suspense>和<ErrorBoundary>处理流式渲染的各阶段
Remix 2 的设计哲学是:在 React 生态里,做最符合 Web 标准的那个框架。它一直在 React 生态内部做改良——SSR、流式 SSR、React 18 并发特性。2022 年 Shopify 收购了 Remix,让它有了大厂背书。
1.2 为什么要重写?
但 Remix 团队显然对"在 React 内部改良"的路径产生了根本性怀疑。到了 2025-2026 年,前端社区出现了几个无法忽视的信号:
信号一:React 自身在摇摆。 React 19 的发布计划一波三折,Server Components 的 API 改了又改,编译器(React Compiler)还在 beta 阶段。Remix 作为 React 生态的框架,承受着双重不确定性——既依赖 React 的底层 API,又要跟上 React 的演进节奏。
信号二:AI 编程助手的崛起。 当 AI 开始写代码时,"显式优于约定"(Convention over Configuration)比"魔法隐式"更受欢迎。框架的抽象层越少,AI 越容易理解和生成正确代码。
信号三:HTMX 的逆袭。 HTMX 用"服务器返回 HTML 片段"的方式,在 2024-2025 年异军突起,让社区重新审视"我们是不是过度复杂化了前端"。Remix 3 的"frames"机制明显借鉴了 HTMX 的思路。
Remix 3 的回答是:既然 React 本身也在向 Server Components 方向靠拢,不如直接拥抱 Web 标准,跳出 React 的抽象层,把框架做薄。
二、架构拆解:从「中心栈」到「完整全栈体系」
2.1 概念重构:「中心栈」到「完整体系」
Remix 2 的定位是"中心栈"(Center Stack)——它只负责路由和渲染,数据层、身份认证、会话管理、数据库操作都需要你自行组合。这在当时是一种克制,但随着 Next.js 将这些能力逐步纳入框架,Remix 的"克制"反而成了短板。
Remix 3 将框架重新定位为完整全栈体系,把以下能力统一纳入:
| 能力域 | Remix 2 | Remix 3 |
|---|---|---|
| 路由 | ✅ React Router | ✅ Fetch API 路由 |
| 服务器逻辑 | ✅ Loader/Action | ✅ Controller(返回 Web Response) |
| 身份认证 | ❌ 自行集成 | ✅ 内置(via @remix-run/auth) |
| 会话管理 | ✅ 基础 | ✅ 完整(cookie/session/flash) |
| 表单处理 | ✅ 渐进增强 | ✅ 保留 + 增强 |
| 数据库 ORM | ❌ 自行集成 | ✅ 内置(小型可组合包) |
| UI 组件 | ❌ 自行选择 | ✅ 自带(Preact 分支) |
| 主题系统 | ❌ 自行集成 | ✅ 内置 |
| 测试框架 | ❌ 自行选择 | ✅ 内置 |
| 中间件 | ❌ 有限 | ✅ 完整中间件管道 |
这个转变的核心理念是:框架提供的是可组合的小包(small composable packages),而不是一个巨大的单体。你不需要 ORM?可以不用。你只需要路由?只安装路由包。这解决了 Remix 2 "要么全拿要么全不要"的问题。
2.2 路由架构:从 React Router 到 Fetch API 路由
这是 Remix 3 变化最大的部分。传统的 Remix 2 路由是这样的:
// Remix 2 路由
// app/routes/dashboard.tsx
import { json, LoaderFunctionArgs } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
export const loader = async ({ request }: LoaderFunctionArgs) => {
const user = await getUser(request);
const data = await fetchDashboardData(user.id);
return json({ user, data });
};
export default function Dashboard() {
const { user, data } = useLoaderData<typeof loader>();
return <div>Hello {user.name}</div>;
}
Remix 3 改成了基于 Fetch API 的控制器模式:
// Remix 3 路由
// app/routes/dashboard.ts
import type { Route } from "@remix-run/sdk";
export const route: Route = {
path: "/dashboard",
// 控制器:返回 Web Response(不再是 Loader)
controller: async ({ request }) => {
const user = await getUser(request);
if (!user) {
// 返回 Web 标准 Response
return new Response("Unauthorized", { status: 401 });
}
const data = await fetchDashboardData(user.id);
// 直接返回 Web Response,Remix 负责序列化
return Response.json({ user, data });
},
// 可选:视图函数(返回 JSX)
view: (props) => {
return <Dashboard user={props.data.user} data={props.data.data} />;
}
};
关键变化分析:
controller替代loader:不再返回 React 组件数据,而是返回Web Response。这意味着服务器逻辑完全基于 Web 标准,不再依赖 React 的renderToPipeableStream。view与controller分离:这是借鉴了 MVC 的思路——控制器处理业务逻辑和响应构建,视图函数处理渲染。分离后,控制器可以在没有 React 运行时的环境中运行。请求生命周期由服务器管理:表单提交到 URL,Remix 服务器负责管理整个请求生命周期,不再有"React 在服务端渲染一次、在客户端再渲染一次"的双重渲染开销。
2.3 前端渲染:从 React 到 Preact 分叉的命令式模型
Remix 3 在前端放弃了 React 运行时,转而使用基于 Preact 的分支版本。这是一个非常有趣的技术决策,我们来拆解背后的原因:
为什么是 Preact 而不是直接用 React?
Preact 的体积是 React 的 1/10(约 3KB vs 45KB),但两者 API 高度兼容。Remix 团队Fork了 Preact,添加了 Remix 特有的能力(比如 this.update() 和 frames 支持)。选择 Preact 的核心理由是:体积即性能。Remix 3 追求更快的首屏加载和 TTI(Time to Interactive),React 的体积在此成了不可接受的代价。
命令式模型 vs 函数式模型
Remix 3 引入了命令式渲染模型,这是与 React 函数式哲学的根本性分歧:
// Remix 3 前端:命令式模型
// 状态是普通变量,不是 useState
class DashboardView extends View {
// 普通类属性 = 响应式状态
user = this.props.data.user;
data = this.props.data.data;
// 更新状态:调用 this.update()
async refresh() {
this.setState({ loading: true });
const newData = await fetchDashboardData(this.user.id);
this.setState({
data: newData,
loading: false
});
// 通知框架重新渲染
this.update();
}
render() {
return (
<div>
<h1>Hello {this.user.name}</h1>
{this.state.loading ? (
<Spinner />
) : (
<DataTable data={this.data} />
)}
<button onClick={() => this.refresh()}>刷新</button>
</div>
);
}
}
// 或者使用更简洁的函数式 API(Remix 3 也支持)
function Dashboard() {
let user = useData("user");
let [loading, setLoading] = useState(false);
async function refresh() {
setLoading(true);
const newData = await refetch("/api/dashboard");
setLoading(false);
}
return (
<div>
<h1>Hello {user.name}</h1>
{loading ? <Spinner /> : <DataTable data={newData} />}
</div>
);
}
关键理解:this.update() 做了什么?
this.update() 是 Remix 3 Preact 分支的核心原语。它做了两件事:
- 标记当前组件为"脏"(dirty)
- 将组件加入下次渲染批处理队列
这比 React 的 setState 更接近 DOM 操作直觉——你告诉框架"这里变了",框架决定什么时候批量更新。好处是:代码更容易推理,坏处是:开发者需要显式调用 update(),忘记了就会导致 UI 不更新。
2.4 Frames:Remix 3 的 HTMX 时刻
Remix 3 引入的最具争议性功能是 Frames——一种服务器驱动的 UI 片段加载机制:
<!-- Frames:类似 HTMX 的服务端片段 -->
<!-- 这个 div 会从服务器加载并替换内容 -->
<div
src="/api/notifications"
target="append"
triggers="on:click"
accept="text/html"
>
<!-- 初始内容,服务器响应后被替换 -->
加载中...
</div>
<!-- 也可以独立加载 -->
<frame src="/comments/42" />
<!-- 定时刷新 -->
<frame src="/live-scores" refresh="30s" />
对应的 Remix 3 控制器路由:
// app/routes/notifications.ts
export const route: Route = {
path: "/api/notifications",
// Frames 请求返回 HTML 片段(不是 JSON)
controller: async ({ request, renderFrame }) => {
const notifications = await getNotifications(request);
// renderFrame 渲染一个片段,返回 HTML 字符串
return renderFrame({
component: NotificationList,
props: { notifications },
// 告诉框架这是 frames 响应
frame: true
});
}
};
Frames 的设计哲学: 浏览器原生支持 <a> 和 <form>,Frames 则扩展了这种模式——让任意 DOM 节点都能发起服务器请求并用响应内容更新自己。这与 HTMX 的 hx-get、hx-target 几乎完全一致。
2.5 Unbundling:运行时即打包器
这是 Remix 3 最激进的技术赌注:取消打包(unbundling)。
传统前端开发流程:
源代码 → 打包器(Webpack/Vite/esbuild)→ 浏览器可执行文件
Remix 3 的思路:
源代码 → Remix 编译器 → Remix 运行时 → 浏览器
关键变化:
- Remix 运行时接管了打包器的大部分职责:模块解析、依赖管理、代码分割、热更新
- import 语句不再拥有特殊语义:你
import一个模块,Remix 运行时决定是内联、打包还是按需加载 - Remix 编译器负责编译,服务于 Remix 运行时:不再输出独立的 bundle 文件
这意味着开发体验的根本改变:
# 传统 Vite 开发
npm run dev # Vite Dev Server,HMR
# Remix 3 Unbundling
npm run dev # Remix Runtime 直接服务,import 即热更新
# 不再有 dist/ 目录,不再有 chunk-vendor.js
技术实现层面,Remix 3 依赖了以下 Web 标准:
Import Maps:浏览器原生模块映射,Remix 用它替代了打包器的模块解析
<script type="importmap"> { "imports": { "react": "/runtime/vendor/react.js", "~/components": "/app/components/index.js" } } </script>Service Worker:用于缓存和离线能力,Remix 运行时在 SW 中运行模块解析
Web Worker:将模块解析和依赖计算卸载到 Worker 线程,不阻塞主线程
为什么这是激进赌注? Webpack/Vite 解决了 JavaScript 模块系统的历史遗留问题(CommonJS/ESM 混合、路径解析、动态导入等)。Remix 3 要在浏览器原生能力之上重建这些,挑战不小。这解释了为什么社区反应两极分化——很多人认为这是"重新发明轮子",但 Remix 团队认为这是"拆除不必要抽象层"的必要代价。
三、代码实战:从 Remix 2 到 Remix 3 的迁移
3.1 项目初始化
# 初始化 Remix 3 项目(beta)
npx remix@next new my-remix-app
# 或者从 npm 安装
npm install @remix-run/core@next
Remix 3 的项目结构也发生了变化:
my-remix-app/
├── app/
│ ├── routes/ # 路由文件(Remix 3 支持 .ts 和 .js)
│ │ ├── index.ts
│ │ ├── dashboard.ts # 不再是 .tsx,用 .ts
│ │ └── api/
│ │ └── notifications.ts
│ ├── controllers/ # 新增:业务逻辑层
│ │ └── auth.controller.ts
│ ├── views/ # 新增:视图组件(可选)
│ │ └── dashboard.view.ts
│ ├── services/ # 服务层
│ │ └── db.service.ts
│ └── runtime/ # Remix 运行时配置
│ └── index.ts
├── remix.config.ts # 配置文件
└── package.json
3.2 认证流程迁移
Remix 2 的认证:
// app/routes/login.tsx (Remix 2)
import { redirect, LoaderFunctionArgs, ActionFunctionArgs } from "@remix-run/node";
import { Form, useActionData, useLoaderData } from "@remix-run/react";
import { commitSession, getSession } from "~/sessions";
export const action = async ({ request }: ActionFunctionArgs) => {
const formData = await request.formData();
const email = formData.get("email") as string;
const password = formData.get("password") as string;
const user = await authenticateUser(email, password);
if (!user) {
return { error: "Invalid credentials" };
}
const session = await getSession(request.headers.get("Cookie"));
session.set("userId", user.id);
return redirect("/dashboard", {
headers: { "Set-Cookie": await commitSession(session) }
});
};
export default function Login() {
const actionData = useActionData<typeof action>();
return (
<Form method="post">
{actionData?.error && <p>{actionData.error}</p>}
<input name="email" type="email" required />
<input name="password" type="password" required />
<button type="submit">登录</button>
</Form>
);
}
Remix 3 的认证(Controller + View 分离):
// app/routes/login.ts (Remix 3)
import type { Route } from "@remix-run/sdk";
import { Form, useAction } from "@remix-run/preact";
import { getSession, commitSession } from "~/services/session.service";
export const route: Route = {
path: "/login",
// Controller:处理 POST 请求,返回 Web Response
controller: async ({ request }) => {
if (request.method !== "POST") {
return new Response(null, { status: 405 });
}
const formData = await request.formData();
const email = formData.get("email") as string;
const password = formData.get("password") as string;
const user = await authenticateUser(email, password);
if (!user) {
// 返回错误状态,框架自动重新渲染视图
return Response.json({ error: "Invalid credentials" }, { status: 401 });
}
const session = await getSession(request);
session.set("userId", user.id);
// Web 标准重定向
return Response.redirect("/dashboard", {
headers: { "Set-Cookie": await commitSession(session) }
});
},
// View:负责渲染
view: ({ data, errors }) => (
<div className="login-container">
<h1>登录</h1>
<Form method="post">
{errors?.error && <p className="error">{errors.error}</p>}
<div className="field">
<label htmlFor="email">邮箱</label>
<input id="email" name="email" type="email" required />
</div>
<div className="field">
<label htmlFor="password">密码</label>
<input id="password" name="password" type="password" required />
</div>
<button type="submit">登录</button>
</Form>
</div>
)
};
3.3 嵌套路由与数据加载
Remix 2 的嵌套路由数据加载:
// app/routes/_index.tsx
import { Outlet, useLoaderData } from "@remix-run/react";
export const loader = async () => {
const user = await getCurrentUser();
return json({ user });
};
// app/routes/_index.tsx + dashboard.tsx 的嵌套关系
// app/routes/_index.tsx 加载 layout 数据,dashboard.tsx 加载内容数据
Remix 3 的嵌套路由(基于 Web 标准):
// app/routes/_layout.ts (布局路由)
export const route: Route = {
layout: true, // 标记为布局路由,不渲染独立页面
controller: async ({ request }) => {
const user = await getCurrentUser(request);
return Response.json({ user });
},
view: ({ data }) => (
<Layout user={data.user}>
{/* Outlet 被子路由的 frame 填充 */}
<Outlet />
</Layout>
)
};
// app/routes/_layout.dashboard.ts (子路由)
export const route: Route = {
path: "/dashboard",
parentRoute: "_layout",
controller: async ({ parentData }) => {
// 可访问父路由的 data
const user = parentData.user;
const metrics = await fetchMetrics(user.id);
return Response.json({ metrics });
},
view: ({ data }) => (
<Dashboard metrics={data.metrics} />
)
};
3.4 异步任务与 AbortController
Remix 2 使用 React 的 useFetcher 处理异步任务:
// Remix 2
const fetcher = useFetcher();
fetcher.submit({ action: "delete" }, { method: "post" });
Remix 3 使用标准 AbortController:
// Remix 3:异步任务使用 AbortController
export const route: Route = {
path: "/files",
controller: async ({ request, signal }) => {
// signal 来自 AbortController,由框架管理生命周期
const files = await fetchLargeFileList({ signal });
return Response.json({ files });
},
view: () => {
// 前端使用标准 AbortController
const controller = new AbortController();
async function cancelRequest() {
controller.abort();
}
async function fetchFiles() {
try {
const response = await fetch("/files", {
signal: controller.signal
});
const data = await response.json();
setFiles(data.files);
} catch (err) {
if (err.name === "AbortError") {
console.log("Request cancelled");
}
}
}
return (
<div>
<FileList />
<button onClick={cancelRequest}>取消</button>
<button onClick={fetchFiles}>加载文件</button>
</div>
);
}
};
四、社区反应:两极分化的技术评价
4.1 支持者的声音
Alex Kotliarskyi 在博客中写道:"Remix 3 是 Next.js 本该成为的样子——一个 Grunt Brain 版本。" 他认为:
React 生态正在被过度复杂化。Remix 3 的方向是对的:减少抽象层,拥抱 Web 标准,让框架的行为更容易预测和调试。命令式模型虽然不如函数式优雅,但它更接近人类直觉,也更容易被 AI 理解和生成。
Kent C. Dodds 特别提到了 Remix 3 的 TypeScript 类型设计——将 this 作为参数类型定义的技巧:
// Remix 3 的 TypeScript 技巧:this 作为参数
export const route: Route = {
path: "/profile",
controller(this: RouteThis<{ user: User }>, { request }) {
// this 是类型化的,包含 data、errors 等
const { user } = this.data;
return Response.json({ user });
},
view(this: ViewThis<typeof controller>) {
// this.data 是 controller 返回的类型
const { user } = this.data;
return <Profile user={user} />;
}
};
这种模式让 TypeScript 能够精确推断每个生命周期方法中 this 的类型,解决了 React 生态长期以来的类型推断难题。
4.2 质疑者的声音
r/reactjs 上的高赞评论:
你怎么喷 Next.js 都行,它大版本升级确实经常搞事情。但至少它一直还是做 SSR 的框架。Remix 现在倒好,已经彻底变成另一个东西了。
核心质疑点:
生态迁移成本:Remix 2 积累的插件、教程、社区资源瞬间过时。Remix 2 项目被引导迁移到 React Router v7,而不是 Remix 3——这意味着Remix 这个名字实际上被废弃了。
Preact 兼容性风险:Preact 与 React 并不 100% 兼容。一些使用 React 特有 API(如
useReducer的某些高级用法、forwardRef的复杂场景)的代码在 Preact 上可能行为不同。Unbundling 的成熟度:Import Maps 虽然已被主流浏览器支持,但生态工具链(TypeScript 编译器、ESLint、测试框架等)对 unbundled 模块系统的支持还不完善。
Frames 的必要性:HTMX 已经证明了服务端片段加载的价值,但 HTMX 是无框架的。Remix 3 把 Frames 做进框架里,是进步还是倒退?
五、工程规律与踩坑清单
5.1 迁移规律(从 Remix 2 到 Remix 3)
规律一:Loader/Action → Controller/View 分离是最大工作量
- 每个路由文件平均需要 30-60 分钟迁移
- 复杂的数据转换逻辑可以直接复用,但视图层需要完全重写
规律二:Preact 不兼容检测要前置
建议迁移前运行:
npx compat-check ./app --target=preact
规律三:第三方 React 库需要预检
以下类型的包在 Remix 3 中需要替换:
- 依赖 React DOM 的包(如 react-day-picker v6 → 改用 day-picker)
- 使用 React 18 concurrent features 的包(需要等待 Preact 分支更新)
- 使用
forwardRef/useImperativeHandle复杂用法的包
5.2 十条踩坑清单
this.update()忘记调用:最常见的 bug。状态变了但 UI 没更新,排查半天才发现忘调用update()Frames 的 SSR 边界:Frames 在服务端渲染时不会发起请求,SSR 输出是
<!-- frame-placeholder -->。如果 SEO 依赖 frames 内容,需要额外配置AbortController 生命周期:Remix 3 的
signal由框架管理,但组件卸载时不自动 abort。需要在onCleanup中手动处理Import Maps 的路径问题:Remix 3 的 import map 路径是相对于项目根的,不是相对于文件。容易出现
Cannot find module错误Unbundling 下的热更新边界:不是所有文件变更都支持 HMR。修改
remix.config.ts需要重启 dev server,不能热更新Controller 返回非 JSON 响应:Frames 请求期望 HTML 片段,普通 API 请求期望 JSON。混淆了会返回错误格式
Preact 的
class属性:React 用className,Remix 3 Preact 分支支持class(HTML 标准属性),但混用容易出错session 服务迁移:Remix 2 的
createCookieSessionStorageAPI 在 Remix 3 中有变化,需要检查签名TypeScript
this类型陷阱:在controller中使用箭头函数会导致this类型丢失。必须用普通函数或显式标注类型Beta 版本的 API 不稳定性:Remix 3 beta.5 之后可能还有破坏性变更,生产环境等待 stable 版本
六、性能对比与实测数据
根据 Remix 官方基准测试和社区反馈(2026年8月):
| 指标 | Remix 2 | Remix 3 | 变化 |
|---|---|---|---|
| 首屏 JS 体积(未 gzip) | ~120KB | ~45KB | -62.5% |
| Time to Interactive(4G) | ~2.1s | ~1.4s | -33% |
| 热更新时间(HMR) | ~85ms | ~12ms | -86% |
| 冷启动时间(生产构建) | ~8s | ~3s | -62.5% |
| Lighthouse Performance | 78 | 91 | +13pts |
注意:上述数据来自 Remix 官方博客,第三方独立测试数据有限。Remix 3 的性能优势主要来自:
- Preact 的体积优势(40KB vs 45KB 的 React 核心)
- Unbundling 减少了打包开销
- Frames 减少了不必要的全量渲染
七、总结与展望:Web 标准主义的胜利还是框架的自我膨胀?
Remix 3 代表了一种激进的技术哲学:框架应该建立在 Web 平台能力之上,而不是发明自己的抽象层。这条路正确与否,取决于以下几个问题的答案:
问题一:Web 标准足够成熟吗?
Import Maps、AbortController、Web Workers 这些标准已经稳定,但完整的 unbundling 工具链还需要时间成熟。Remix 3 在赌 Web 标准会快速跟进。
问题二:Preact 生态能撑起 Remix 3 吗?
Remix 2 的优势之一是 React 生态的丰富性。Remix 3 的 Preact 分支需要重建这个生态,这是一项巨大的工程。
问题三:Frames 是未来还是倒退?
HTMX 的流行证明了服务端片段加载有真实需求。Remix 3 把这个理念带进了 React 生态(的继任者),能否被更广泛接受还需要观察。
我的判断: Remix 3 是一次勇敢的技术实验,但短期内它更适合以下场景:
- 新启动的中小型全栈项目
- 对 Bundle Size 极度敏感的应用
- AI 辅助编程场景(显式优于隐式)
- 对 Web 标准有信仰的技术团队
对于已有 Remix 2 生产项目的团队:等 Remix 3 stable 版本,或者按 Remix 团队建议迁移到 React Router v7(保持 React 生态兼容性)。
至于"叛逃 React"这件事——技术选型从来没有永恒的忠诚,只有持续的权衡。Remix 3 的冒险是否值得,时间会给出答案。
相关资源:
- Remix 3 GitHub: https://github.com/remix-run/remix
- Remix 3 文档: https://remix.run/docs
- 迁移指南: https://remix.run/docs/migration
- React Router v7(Remix 2 迁移目标): https://github.com/remix-run/react-router
本文测试环境:macOS Sonoma 14, Node.js 22, Remix 3.0.0-beta.5