Remix 3 深度拆解:从 React 叛逃到 Web 平台原生——2026年最重磅前端框架事件完整解析
一、引言:这不是一次普通的大版本更新
2026年8月,前端圈迎来了一颗真正的深水炸弹。
Remix 官方正式发布了 v3.0.0-beta.5 预览版,这是一次从底层彻底重构的版本更新。更准确地说,这是 Remix 团队对自身存在理由的一次重新审视和回答:Remix 决定放弃 React 生态的庇护,走向 Web 平台原生能力。
这不是修修补补,不是加几个新 API。这是架构级的范式转移。
Remix 成立于2020年,由前 Twitter 工程师 Michael Jackson 和 Ryan Florence 共同创建。两人此前最出名的项目是 React Router——一个被下载了近10亿次的 React 路由库。2022年11月,Remix 被 Shopify 收购,成为其 Hydrogen 电商框架的技术底座。
从诞生到被收购,Remix 一直是"React 全栈框架"的代表。它以服务端渲染、渐进增强和数据加载优化著称,是 Next.js 最主要的竞争对手之一。
然而,Remix 3 的发布,让这一切发生了变化。
Remix 3 不再依赖 React。它保留了 JSX 语法,但移除了 React 运行时。路由基于 Fetch API,响应返回 Web Response 对象,表单提交到 URL。框架的核心逻辑发生了根本性转变:从"React 的服务端渲染方案"变成了"Web 平台原生的全栈框架"。
本文将从架构原理、核心变化、代码实战、生产踩坑等多个维度,深度拆解这次 Remix 叛逃 React 的前因后果,以及它对整个前端生态的深远影响。
二、背景:Remix 为何而战,Remix 的前世今生
2.1 从 React Router 到 Remix:两条完全不同的人生轨迹
要理解 Remix 3 的叛逃,首先需要理解 Remix 团队对 Web 平台的执着。
Michael Jackson 和 Ryan Florence 在创建 Remix 之前,花了多年时间围绕 React 构建工具。React Router 是他们最成功的作品——几乎每个 React 应用都在用。它解决的问题看似简单:URL 到组件的映射。但它真正解决的是:如何让 Web 的天然能力(书签、后退按钮、SEO)与 React 的组件模型优雅共存。
这段经历塑造了 Remix 团队的核心世界观:React 是一个优秀的 UI 库,但 Web 平台本身已经足够强大。框架应该利用 Web 平台的能力,而不是用 JavaScript 重新发明一切。
这一理念在 Remix 1 中体现得淋漓尽致。Remix 1 的设计哲学是"渐进增强"(Progressive Enhancement):表单、链接、加载状态——这些 Web 1.0 就有的原生能力,Remix 将它们重新带回现代 SPA 开发中。Remix 不需要客户端路由就能工作,因为它优先使用原生表单提交和页面导航。
// Remix 1 的经典数据加载模式
// app/routes/users.tsx
export async function loader({ request }: LoaderFunctionArgs) {
const users = await db.users.findMany();
return json({ users });
}
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const name = formData.get("name");
await db.users.create({ name });
return redirect("/users");
}
// 组件中直接使用原生表单,无需 useState
export default function Users() {
const { users } = useLoaderData<typeof loader>();
return (
<div>
<h1>用户列表</h1>
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
{/* 原生表单提交,自动处理渐进增强 */}
<Form method="post">
<input name="name" placeholder="输入用户名" />
<button type="submit">添加</button>
</Form>
</div>
);
}
这种模式在当时是革命性的。它让表单提交不需要 useState、不需要 onSubmit 处理器、不需要 fetch 调用——只需要一个 <Form> 组件,数据会自动发送到服务端 action,页面会自动刷新(带或不带 JavaScript 均可)。
2.2 Shopify 收购:从独立框架到商业公司的一部分
2022年11月,Remix 被 Shopify 收购。Shopify 使用 React Router 构建其 Hydrogen 框架——一个专为 Shopify 商家设计的 React 全栈电商框架。收购 Remix 是为了让 Hydrogen 的开发体验更上一层楼。
在 Shopify 的管理下,Remix 继续独立运营:框架保持开源,团队保持独立,Michael Jackson 继续担任 CEO。但商业化的压力和 Shopify 的战略需求,不可避免地影响了 Remix 的发展方向。
Shopify 需要的 Remix 是一个可以在 Cloudflare Workers、Deno Deploy、Vercel Edge Functions 等各种边缘运行时上高性能运行的框架。Remix 2.x 的架构为此做了大量优化:流式 SSR、边缘适配器、更轻量的运行时。
但同时,React 的约束也越来越明显。
三、核心变化:Remix 3 到底变了什么?
3.1 移除 React 运行时:这是最核心的变化
Remix 3 最大的变化,也是引发最多讨论的变化:移除了 React 运行时依赖。
这不意味着 Remix 不再支持 JSX。Remix 3 仍然使用 JSX 作为模板语法——毕竟 JSX 是一种 HTML-like 的语法糖,它可以编译成任何东西。但渲染引擎从 React 变成了 Preact 的一个定制分支版本。
// Remix 3 的组件写法
// 不再需要从 'react' 导入任何东西
import { h, Fragment } from 'remix';
// 状态只是一个普通变量,不再是 useState
let count = 0;
export default function Counter() {
return (
<div>
<p>计数: {count}</p>
{/* 命令式更新,this.update() 触发重新渲染 */}
<button onClick={() => {
count++;
this.update(); // ← Remix 3 的新 API
}}>
+1
</button>
</div>
);
}
对比 React 的写法:
// React 写法
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>计数: {count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
);
}
两个模式的本质区别在于:
| 维度 | React | Remix 3 |
|---|---|---|
| 状态管理 | useState Hook,不可变更新 | 普通变量,命令式更新 |
| 渲染触发 | setState 自动触发 | this.update() 手动触发 |
| 虚拟 DOM | 有,React 协调算法 | 无,直接 DOM 操作 |
| 运行时大小 | React + ReactDOM,约45KB | Preact 定制版,约3KB |
| 初始化成本 | JS Bundle 加载后才能运行 | 几乎即时 |
Remix 团队认为:虚拟 DOM 是一个历史遗留方案。 在2013年,虚拟 DOM 解决了"如何高效更新 DOM"的问题。但2026年的 Web 平台已经有了 requestAnimationFrame、 Web Components 的 Custom Elements、甚至 signals 响应式原语。Remix 不再需要虚拟 DOM 来做协调——直接操作 DOM 在现代浏览器中已经足够快。
3.2 Fetch API 路由:彻底拥抱 Web 标准
Remix 3 的路由系统从 React Router 的自定义实现,迁移到了 Fetch API 路由。
// Remix 3 路由文件
// app/routes/users.ts
// 不再使用 loader/action 函数
// 改为标准的 Request/Response 模式
export async function GET(request: Request) {
// 直接返回 Response,而非 json() 包装
const users = await db.users.findMany();
return new Response(JSON.stringify({ users }), {
headers: { 'Content-Type': 'application/json' }
});
}
export async function POST(request: Request) {
const formData = await request.formData();
const name = formData.get('name') as string;
const user = await db.users.create({ name });
// 302 重定向使用标准 Web Response
return new Response(null, {
status: 302,
headers: { 'Location': '/users' }
});
}
这是完全符合 Web 标准的 API 设计。每个路由文件变成了一个小型的 Serverless 函数,可以部署到任何支持 Fetch API 的运行时上:Cloudflare Workers、Deno Deploy、Bun 运行时、甚至普通的 Node.js HTTP 服务器。
路由层的变化带来了几个重要影响:
第一,SSR 逻辑更清晰。 之前 Remix 的 loader 和 action 是 Remix 特有的抽象。现在每个路由就是一组标准的 HTTP 处理器,调试时可以直接在浏览器 DevTools 的 Network 面板中看到原始的请求/响应,不再需要理解 Remix 的数据加载协议。
第二,边缘部署更简单。 Cloudflare Workers 支持的正是 Fetch API 事件处理器。Remix 3 的路由可以直接作为 Worker 脚本运行,不需要额外的适配层。
// Remix 3 在 Cloudflare Workers 上的部署
// 这就是一个完整的 Remix 3 应用
export default {
async fetch(request: Request): Promise<Response> {
return handleRequest(request);
}
};
// handleRequest 由 Remix 3 框架生成
// 它根据 URL 匹配路由文件,调用对应的 GET/POST 等处理器
第三,测试更容易。 你可以直接用 fetch() 或 curl 测试任何 Remix 3 路由,不需要启动整个 Remix 应用:
# 直接测试 Remix 3 路由
curl -X POST -d "name=张三" http://localhost:3000/users
# 对比 Remix 2.x:需要启动应用,通过 Playwright 测试
3.3 Frames:服务端驱动的 UI 新范式
Remix 3 引入了一个全新的概念:Frames。这是 Remix 3 最具创新性的设计,也是最让人"眼前一亮"的部分。
Frames 的灵感来自现代浏览器的 <iframe> 和服务端包含(SSI),但 Remix 重新设计了它,使其适合现代全栈应用。
<!-- Remix 3 Frame 的基本用法 -->
<!-- 在任意组件中嵌入服务端渲染的独立片段 -->
<Frame src="/comments/42" />
<!-- 或带参数 -->
<Frame src="/comments/42?page=2" loading="lazy" />
<!-- Frame 内容独立加载和重新加载 -->
<!-- 点击"下一页"只需要更新 Frame,不刷新整个页面 -->
Frames 的核心特点是:
- 独立加载:每个 Frame 像一个沙盒化的 iframe,有自己的数据生命周期
- 服务端驱动:Frame 的内容由服务端渲染,服务端决定展示什么
- 渐进增强:浏览器禁用了 JavaScript?Frame 仍然正常工作(作为原生 iframe)
- 并行加载:多个 Frame 可以并行请求,后端可以并行处理它们的数据
// app/routes/comments.ts 的 Frame 版本
// 不再使用 useLoaderData,改用 Frame 的服务端逻辑
export async function GET(request: Request) {
const url = new URL(request.url);
const page = parseInt(url.searchParams.get('page') || '1');
const comments = await db.comments.findMany({
where: { postId: 42 },
skip: (page - 1) * 20,
take: 20
});
// 返回完整的 HTML 片段,而非 JSON
// Remix 3 的 Frame 渲染引擎会自动处理
return new Response(renderCommentsHTML(comments, page), {
headers: { 'Content-Type': 'text/html' }
});
}
Frames 的出现,意味着 Remix 3 可以在不使用 React 的情况下,实现过去需要复杂状态管理才能做到的事情:局部更新、并行数据加载、错误边界隔离。
3.4 事件系统:从 onClick 到统一 on 属性
Remix 3 简化了事件系统。React 中有大量的 onXxx 事件处理器(onClick、onChange、onSubmit、onFocus……),Remix 3 统一为 on 属性:
// React 写法
<input
onChange={handleChange}
onFocus={handleFocus}
onBlur={handleBlur}
/>
// Remix 3:统一事件处理
<input
on="change"={handleChange}
on="focus"={handleFocus}
on="blur"={handleBlur}
/>
// 或者批量处理
<div on="click submit"={handleInteraction}>
<button>提交</button>
</div>
这看起来是语法糖,但实际上简化了事件委托的实现。Remix 3 在底层使用原生的事件委托,不再需要 React 的合成事件系统。
3.5 异步任务:从 useEffect 到 AbortController
Remix 3 的异步任务管理迁移到了 Web 标准 AbortController:
// Remix 3 的异步任务
// 不再需要 useEffect cleanup
let controller: AbortController;
async function search(query: string) {
// 取消上一次请求
if (controller) {
controller.abort();
}
controller = new AbortController();
try {
const results = await fetch(`/api/search?q=${query}`, {
signal: controller.signal
}).then(r => r.json());
this.setState({ results });
} catch (err) {
if (err.name === 'AbortError') {
// 请求被取消,忽略
return;
}
throw err;
}
}
// 取消按钮
<button onClick={() => controller?.abort()}>取消</button>
四、架构分析:Remix 3 的技术决策背后的逻辑
4.1 为什么抛弃 React?
Remix 团队的决定,表面上看起来像是"背叛",但实际上背后有非常务实的工程考量。
第一,React 的运行时成本。 React 18 + ReactDOM 的 gzipped 大小约 45KB(压缩后),这在桌面端不是什么问题。但在边缘计算场景中,每 KB 都直接影响冷启动时间。Cloudflare Workers 的免费套餐限制为 10ms CPU 时间,Vercel Edge Functions 的最大响应时间与 Bundle 大小成正比。Remix 3 切换到 Preact 后,运行时体积降到约 3KB,冷启动时间可以缩短 50-70%。
第二,React 的并发特性在 Remix 场景下用处不大。 React 18 引入的并发渲染(Concurrent Rendering)、Suspense for Data Fetching、Transition——这些特性是针对纯客户端应用设计的。Remix 是一个服务端渲染框架,数据在服务端就加载好了,不需要客户端再 Suspense。"服务端先加载完整数据 → 流式发送到客户端"才是 Remix 的模式,而非 React 的"先渲染 → 再请求数据"。
第三,React 的更新节奏与 Remix 的方向不一致。 React 的路线图围绕"更好的客户端体验"设计:Server Components、Actions、use() Hook……这些方向与 Remix 的"利用 Web 平台原生能力"哲学越来越远。Remix 发现自己需要越来越多的"适配层"代码来弥合 React 和 Web 标准之间的差异。与其持续适配,不如直接基于 Web 标准构建。
第四,Bun 和 Deno 的崛起让标准变得更重要。 2026年,Bun 已经成为了主流的 JavaScript 运行时(Remix 本身也从 Zig 重写到了 Rust,见另一篇热门文章《Bun 从 Zig 到 Rust 的11天重构》)。Bun 原生支持 Fetch API、Web Standards API 和部分 Web Worker API。Remix 3 基于这些标准构建,意味着可以在 Bun、Deno 和 Node.js 上无缝运行。
4.2 迁移策略:Remix 2 到 Remix 3 的渐进迁移
Remix 团队深知,大规模迁移是开发者最头疼的事情。因此他们提供了清晰的迁移路径:
第一,Remix 3 仍然支持 React 组件。 迁移不是一蹴而就的。你可以在同一个应用中混合使用 Remix 3 的新组件和旧的 React 组件。Remix 3 提供了一个兼容层,让你可以在新架构中使用 React 组件。
// Remix 3 兼容层:继续使用 React 组件
import { createReactComponent } from 'remix/react-compat';
const OldReactComponent = createReactComponent(OldReactComponent);
// OldReactComponent 可以像 Remix 3 原生组件一样使用
第二,提供 codemod 自动迁移工具。 Remix 官方提供了 remix-codemod 工具,可以自动处理大部分代码迁移:
# 安装迁移工具
npx remix-codemod@latest
# 运行迁移
npx remix-codemod remix-3 ./src/routes
# codemod 会处理:
# - loader/action → GET/POST 函数
# - useLoaderData → 直接使用 Frame
# - json() → new Response()
# - Form → 原生 <Form> 或 Frame
第三,Remix 2 应用无需改动即可继续运行。 Remix 3 不会强制废弃 Remix 2 API,至少在 3.x 的整个生命周期内,Remix 2 的 API 都可以继续使用。你可以按路由、按模块逐步迁移,而不是一次性重写整个应用。
五、实战代码:从零构建一个 Remix 3 全栈应用
下面通过一个完整的代码示例,展示如何使用 Remix 3 从零构建一个简单的博客系统。
5.1 项目初始化
# 使用 Remix 3 CLI 创建项目
npx create-remix@latest my-blog --template remix-3
cd my-blog
# 安装依赖(Remix 3 不再需要 react 和 react-dom)
npm install
# 启动开发服务器
npm run dev
5.2 数据库配置(使用 Drizzle ORM + SQLite)
// db/schema.ts
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
export const posts = sqliteTable('posts', {
id: integer('id').primaryKey({ autoIncrement: true }),
title: text('title').notNull(),
content: text('content').notNull(),
author: text('author').notNull(),
createdAt: integer('created_at', { mode: 'timestamp' })
.$defaultFn(() => new Date())
});
export type Post = typeof posts.$inferSelect;
// db/index.ts
import Database from 'better-sqlite3';
import { drizzle } from 'drizzle-orm/better-sqlite3';
import * as schema from './schema';
const sqlite = new Database('blog.db');
export const db = drizzle(sqlite, { schema });
5.3 路由:文章列表页
// app/routes/index.ts
// 文章列表路由(GET /)
import { db } from '~/db';
import { posts } from '~/db/schema';
import { eq, desc } from 'drizzle-orm';
export async function GET(request: Request): Promise<Response> {
// 服务端直接查询数据库
const allPosts = await db
.select()
.from(posts)
.orderBy(desc(posts.createdAt))
.limit(20);
// 渲染 HTML(Remix 3 提供轻量级模板引擎)
const html = renderHTML(`
<h1>博客文章</h1>
<ul>
${allPosts.map(post => `
<li>
<a href="/posts/${post.id}">${escapeHtml(post.title)}</a>
<span>by ${escapeHtml(post.author)}</span>
</li>
`).join('')}
</ul>
<a href="/posts/new">写文章</a>
`);
return new Response(html, {
headers: { 'Content-Type': 'text/html' }
});
}
// 简单的 HTML 转义,防止 XSS
function escapeHtml(text: string): string {
return text
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"');
}
// Remix 3 提供的基础模板渲染函数
function renderHTML(body: string): string {
return `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>我的博客</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 800px; margin: 0 auto; padding: 2rem; }
ul { list-style: none; padding: 0; }
li { padding: 0.5rem 0; border-bottom: 1px solid #eee; }
a { color: #0070f3; text-decoration: none; }
a:hover { text-decoration: underline; }
</style>
</head>
<body>
${body}
</body>
</html>`;
}
5.4 路由:文章详情页
// app/routes/posts.$postId.ts
// 文章详情路由(GET /posts/:postId)
import { db } from '~/db';
import { posts } from '~/db/schema';
import { eq } from 'drizzle-orm';
export async function GET(
request: Request,
{ params }: { params: { postId: string } }
): Promise<Response> {
const postId = parseInt(params.postId);
if (isNaN(postId)) {
return new Response('文章不存在', { status: 404 });
}
const [post] = await db
.select()
.from(posts)
.where(eq(posts.id, postId))
.limit(1);
if (!post) {
return new Response('文章不存在', { status: 404 });
}
// 渲染 Markdown(使用 marked 库)
const marked = (await import('marked')).default;
const contentHtml = marked(post.content);
const html = renderHTML(`
<article>
<h1>${escapeHtml(post.title)}</h1>
<p class="meta">作者:${escapeHtml(post.author)} |
${post.createdAt?.toLocaleDateString('zh-CN')}</p>
<div class="content">${contentHtml}</div>
</article>
<hr>
<a href="/">← 返回列表</a>
<!-- Remix 3 Frames:嵌入评论区 -->
<section class="comments">
<h2>评论区</h2>
<Frame src="/frames/comments?postId=${postId}" loading="lazy" />
</section>
`);
return new Response(html, {
headers: {
'Content-Type': 'text/html',
'Cache-Control': 'public, max-age=3600' // 缓存1小时
}
});
}
5.5 路由:创建文章(表单提交)
// app/routes/posts.new.ts
// 创建文章路由(GET 显示表单,POST 处理提交)
import { db } from '~/db';
import { posts } from '~/db/schema';
export async function GET(_request: Request): Promise<Response> {
const html = renderHTML(`
<h1>写文章</h1>
<form method="post">
<div>
<label>标题:<input name="title" required /></label>
</div>
<div>
<label>作者:<input name="author" required /></label>
</div>
<div>
<label>内容:<textarea name="content" rows="10" required></textarea></label>
</div>
<button type="submit">发布</button>
</form>
<a href="/">取消</a>
`);
return new Response(html, {
headers: { 'Content-Type': 'text/html' }
});
}
export async function POST(request: Request): Promise<Response> {
const formData = await request.formData();
const title = formData.get('title') as string;
const author = formData.get('author') as string;
const content = formData.get('content') as string;
// 表单验证
if (!title || !author || !content) {
return new Response('所有字段必填', { status: 400 });
}
if (title.length > 200) {
return new Response('标题不能超过200字', { status: 400 });
}
// 写入数据库
const [newPost] = await db
.insert(posts)
.values({ title, author, content })
.returning();
// 302 重定向到文章详情页
return new Response(null, {
status: 302,
headers: { 'Location': `/posts/${newPost.id}` }
});
}
5.6 Frame:评论区
// app/routes/frames.comments.ts
// Frame 版本评论区(支持独立加载)
import { db } from '~/db';
import { comments } from '~/db/schema';
import { eq, desc } from 'drizzle-orm';
export async function GET(request: Request): Promise<Response> {
const url = new URL(request.url);
const postId = parseInt(url.searchParams.get('postId') || '0');
const page = parseInt(url.searchParams.get('page') || '1');
const pageSize = 10;
const allComments = await db
.select()
.from(comments)
.where(eq(comments.postId, postId))
.orderBy(desc(comments.createdAt));
const totalPages = Math.ceil(allComments.length / pageSize);
const pageComments = allComments.slice(
(page - 1) * pageSize,
page * pageSize
);
const html = `
${pageComments.length === 0
? '<p>还没有评论,来抢沙发!</p>'
: pageComments.map(c => `
<div class="comment">
<strong>${escapeHtml(c.author)}</strong>
<p>${escapeHtml(c.content)}</p>
<small>${c.createdAt?.toLocaleString('zh-CN')}</small>
</div>
`).join('')
}
${totalPages > 1 ? `
<div class="pagination">
${page > 1 ? `<a href="?postId=${postId}&page=${page-1}">上一页</a>` : ''}
<span>第 ${page} / ${totalPages} 页</span>
${page < totalPages ? `<a href="?postId=${postId}&page=${page+1}">下一页</a>` : ''}
</div>
` : ''}
<form method="post" class="comment-form">
<h3>发表评论</h3>
<input name="author" placeholder="昵称" required />
<textarea name="content" placeholder="说点什么..." required></textarea>
<button type="submit">提交</button>
</form>
<style>
.comment { border-bottom: 1px solid #eee; padding: 0.5rem 0; }
.comment-form { margin-top: 1rem; }
.comment-form input, .comment-form textarea { display: block; width: 100%; margin: 0.5rem 0; padding: 0.5rem; }
.pagination { margin: 1rem 0; }
.pagination a { margin-right: 1rem; }
</style>
`;
// Frame 返回纯 HTML,不包含完整文档
return new Response(html, {
headers: { 'Content-Type': 'text/html' }
});
}
export async function POST(request: Request): Promise<Response> {
const formData = await request.formData();
const postId = parseInt(formData.get('postId') as string);
const author = formData.get('author') as string;
const content = formData.get('content') as string;
await db.insert(comments).values({
postId,
author,
content,
createdAt: new Date()
});
// POST 后重定向回当前 Frame(防止表单重复提交)
return new Response(null, {
status: 302,
headers: { 'Location': `/frames/comments?postId=${postId}&page=1` }
});
}
5.7 部署到 Cloudflare Workers
// wrangler.toml(Cloudflare Workers 配置)
name = "my-remix-blog"
main = "build/server/index.js"
compatibility_date = "2026-08-01"
# Remix 3 生成的构建产物可以直接作为 Worker 运行
# 无需额外的适配器
# 构建 Remix 3 应用
npm run build
# 部署到 Cloudflare Workers
npx wrangler deploy
# 整个博客系统可以在 Cloudflare 全球边缘网络上运行
# 冷启动时间 < 10ms(得益于 Preact 轻量运行时)
# 全球 CDN 加速,静态资源自动缓存
六、性能对比:Remix 3 vs Remix 2 vs Next.js
6.1 冷启动时间
Remix 3 最大的性能收益来自冷启动时间的显著缩短。
| 运行时 | Remix 2 | Remix 3 | 提升 |
|---|---|---|---|
| Cloudflare Workers | 120ms | 15ms | 87.5% |
| Vercel Edge | 80ms | 12ms | 85% |
| Deno Deploy | 60ms | 8ms | 86.7% |
| Node.js (Bun) | 25ms | 18ms | 28% |
6.2 Bundle 大小对比
| 依赖 | 大小(gzipped) |
|---|---|
| React 18 + ReactDOM | ~45KB |
| Preact(Remix 3 定制版) | ~3KB |
| React Router v6 | ~12KB |
| Remix 3 路由引擎 | ~8KB |
| Remix 2 总运行时 | ~57KB |
| Remix 3 总运行时 | ~11KB |
Remix 3 的总运行时比 Remix 2 小了约 80%。
6.3 实际 Benchmark
Remix 团队在官方博客中公布了一组实测数据(相同硬件环境,100并发):
测试场景:文章列表页面(含20篇文章摘要)
Remix 2:
- TTFB(首字节时间):85ms
- FCP(首次内容绘制):210ms
- LCP(最大内容绘制):340ms
- TTI(可交互时间):420ms
Remix 3:
- TTFB:12ms ↓ 85.9%
- FCP:95ms ↓ 54.8%
- LCP:180ms ↓ 47.1%
- TTI:220ms ↓ 47.6%
6.4 内存占用
| 指标 | Remix 2 | Remix 3 |
|---|---|---|
| 峰值内存 | 48MB | 22MB |
| JS 堆大小 | 32MB | 11MB |
| DOM 节点数 | 1,204 | 823 |
七、生产踩坑清单:15条来自一线的经验总结
基于 Remix 3 beta 阶段的开发者反馈,以下是在生产环境中使用 Remix 3 需要注意的关键问题:
7.1 迁移相关(5条)
json()已被移除:loader中返回json({ data })的写法不再有效,需要改为new Response(JSON.stringify({ data }), { headers: { 'Content-Type': 'application/json' } }),或使用 Remix 3 提供的json()兼容辅助函数。useLoaderData的替代方案:Remix 3 不再需要useLoaderData。数据在服务端加载,页面本身就是 SSR 的输出。如果需要客户端重新加载数据,使用Frame的src属性传入 URL。Form组件行为变化:Remix 2 的<Form>会拦截提交并通过 XHR 发送。Remix 3 的<Form>直接是原生表单,提交会触发页面刷新。如需保持无刷新体验,改用Frame。React 组件需要显式导入:如果你在 Remix 3 应用中使用了旧的 React 组件,需要通过
createReactComponent包装,否则无法正常工作。类型定义更新:
LoaderFunctionArgs、ActionFunctionArgs等 Remix 特有的类型已被移除。改用标准的Request类型和URL对象的params。
7.2 架构相关(5条)
Frames 的 SEO 限制:Frame 内部的内容对搜索引擎不可见(因为它们是独立加载的)。如果评论区需要 SEO,需要把评论数据放在主路由的 SSR 中一并渲染。
this.update()的作用域陷阱:this.update()只能在组件方法中使用,不能在普通回调函数中使用。这在嵌套组件中尤其容易出错。AbortController 的全局状态管理:每个需要取消异步请求的组件都需要管理自己的
AbortController实例。当组件卸载时,如果还有未完成的请求,必须主动调用abort()。Remix 3 的错误边界在路由层:每个路由文件可以导出一个
ErrorBoundary,但在 Frame 中使用需要特殊处理,因为 Frame 是独立的渲染上下文。Session/Cookie 管理的迁移:
remix-utils库中提供的createCookieSessionStorage等工具在 Remix 3 中仍可用,但需要使用 Web 标准Headers而非 Remix 特有的createHeadersFromAppendList。
7.3 部署相关(5条)
Cloudflare Workers 的大小限制:Cloudflare Workers 免费套餐限制为 1MB,Pro 套餐为 10MB。Remix 3 的核心运行时约 11KB,很安全,但如果你的应用包含了大量服务端模板,仍需注意大小。
Node.js 兼容性问题:Remix 3 的某些 API(如
Response.json())在旧版 Node.js(<18)中不可用。部署到 Node.js 环境时,确保使用 Node.js 18+ 或 Bun。Deno Deploy 的特殊处理:Deno Deploy 对某些 Web 标准 API 的实现与浏览器略有不同,特别是
Request的某些构造方法。部署前需要仔细测试。数据库连接池:Remix 3 在边缘运行时上运行,每个请求可能来自不同的边缘节点。不要在请求外部创建数据库连接(这是常见的反面模式)。使用 Drizzle ORM 的连接池或使用 PlanetScale/Turso 这样的边缘数据库。
构建产物结构变化:Remix 3 的构建产物不再区分
server和client。构建产物是一个可以在任何支持 ES Modules 的运行时运行的模块化包。这意味着你不能再简单地把构建产物放到 CDN 上——它需要在一个支持 ES Module 的运行时中执行。
八、未来展望:Remix 3 对前端生态意味着什么?
8.1 Web 平台原生化的趋势
Remix 3 的方向,折射出一个更大的趋势:Web 平台原生能力的崛起,正在重塑 JavaScript 框架的竞争格局。
过去,前端框架需要"重新发明"很多 Web 平台已有的能力:路由、表单、组件封装、状态管理。但随着 Web 标准的发展(Fetch API、Custom Elements、Declarative Shadow DOM、View Transitions API……),这些能力正在被纳入浏览器原生支持。
React 最早意识到了这个问题并做出了回应:React 19 引入了 Server Components、Actions、use() Hook——本质上是在 React 生态内重新实现 Web 标准的某些能力。
Remix 的选择则不同:与其在框架层适配 React,不如直接用 Web 标准。
这代表了两种不同的哲学路线:
- React 路线:继续深化框架能力,用 React 的方式解决所有问题
- Remix 路线:拥抱 Web 标准,让框架变得更薄、更通用
8.2 对 Next.js 的影响
Next.js 是 Remix 最主要的竞争对手。Remix 3 的发布,会给 Next.js 带来压力。
Next.js 当前的优势在于生态:App Router、Server Components、React 的庞大社区。但 Next.js 的劣势在于体积:Next.js 13+ 的 App Router 在边缘运行时上的性能一直被人诟病。
如果 Remix 3 能够在保持开发体验的同时,提供更好的边缘性能,它可能会吸引一批对性能敏感、但又不想放弃全栈框架体验的开发者。
不过,Next.js 拥有 Vercel 的全力支持,以及庞大的企业用户基础。Remix 在 Shopify 之外的企业采用率,一直是其短板。
8.3 开源生态的未来
Remix 3 的另一个悬念是:Shopify 还会继续投入吗?
Remix 最初被 Shopify 收购,是为了让 Hydrogen 更好用。但 Remix 3 移除了 React——而 Hydrogen 本身就是基于 React 构建的。这意味着 Shopify 可能需要重新评估 Remix 对其战略的价值。
如果 Shopify 的支持减弱,Remix 3 的未来将更加依赖社区。这对于一个正在经历重大转型的框架来说,既是风险,也是机会。
九、总结
Remix 3 是一次勇敢的实验。
它选择了最难的那条路:不再依赖 React,拥抱 Web 平台原生能力,用更轻量的运行时提供更好的性能。对于那些被 React 生态复杂性困扰的开发者,对于那些需要在边缘运行时上部署高性能应用的团队,Remix 3 提供了一个令人兴奋的新选择。
但它也带来了显著的学习曲线和迁移成本。对于已经大规模使用 React 的团队来说,从头学习一个新的框架范式并不容易。
无论如何,Remix 3 代表了一个重要的方向性信号:Web 平台正在变得更加强大,框架应该服务于平台,而不是反过来。
2026年的前端框架战争,才刚刚开始。
参考来源:
- Remix 3 官方公告与 v3.0.0-beta.5 Release Notes
- Michael Jackson "Remix 3: The Future of Web Frameworks" 技术分享
- Cloudflare Workers 边缘运行时性能基准测试
- Shopify Hydrogen 框架与 Remix 集成文档
- Web Standards — Fetch API, Web Response, AbortController 规范文档