编程 Remix 3 深度拆解:从 React 叛逃到 Web 平台原生——2026年最重磅前端框架事件完整解析

2026-08-13 20:44:58 +0800 CST views 7

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>
  );
}

两个模式的本质区别在于:

维度ReactRemix 3
状态管理useState Hook,不可变更新普通变量,命令式更新
渲染触发setState 自动触发this.update() 手动触发
虚拟 DOM有,React 协调算法无,直接 DOM 操作
运行时大小React + ReactDOM,约45KBPreact 定制版,约3KB
初始化成本JS Bundle 加载后才能运行几乎即时

Remix 团队认为:虚拟 DOM 是一个历史遗留方案。 在2013年,虚拟 DOM 解决了"如何高效更新 DOM"的问题。但2026年的 Web 平台已经有了 requestAnimationFrameWeb ComponentsCustom 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 的 loaderaction 是 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 的核心特点是:

  1. 独立加载:每个 Frame 像一个沙盒化的 iframe,有自己的数据生命周期
  2. 服务端驱动:Frame 的内容由服务端渲染,服务端决定展示什么
  3. 渐进增强:浏览器禁用了 JavaScript?Frame 仍然正常工作(作为原生 iframe)
  4. 并行加载:多个 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 事件处理器(onClickonChangeonSubmitonFocus……),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, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;');
}

// 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 2Remix 3提升
Cloudflare Workers120ms15ms87.5%
Vercel Edge80ms12ms85%
Deno Deploy60ms8ms86.7%
Node.js (Bun)25ms18ms28%

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 2Remix 3
峰值内存48MB22MB
JS 堆大小32MB11MB
DOM 节点数1,204823

七、生产踩坑清单:15条来自一线的经验总结

基于 Remix 3 beta 阶段的开发者反馈,以下是在生产环境中使用 Remix 3 需要注意的关键问题:

7.1 迁移相关(5条)

  1. json() 已被移除loader 中返回 json({ data }) 的写法不再有效,需要改为 new Response(JSON.stringify({ data }), { headers: { 'Content-Type': 'application/json' } }),或使用 Remix 3 提供的 json() 兼容辅助函数。

  2. useLoaderData 的替代方案:Remix 3 不再需要 useLoaderData。数据在服务端加载,页面本身就是 SSR 的输出。如果需要客户端重新加载数据,使用 Framesrc 属性传入 URL。

  3. Form 组件行为变化:Remix 2 的 <Form> 会拦截提交并通过 XHR 发送。Remix 3 的 <Form> 直接是原生表单,提交会触发页面刷新。如需保持无刷新体验,改用 Frame

  4. React 组件需要显式导入:如果你在 Remix 3 应用中使用了旧的 React 组件,需要通过 createReactComponent 包装,否则无法正常工作。

  5. 类型定义更新LoaderFunctionArgsActionFunctionArgs 等 Remix 特有的类型已被移除。改用标准的 Request 类型和 URL 对象的 params

7.2 架构相关(5条)

  1. Frames 的 SEO 限制:Frame 内部的内容对搜索引擎不可见(因为它们是独立加载的)。如果评论区需要 SEO,需要把评论数据放在主路由的 SSR 中一并渲染。

  2. this.update() 的作用域陷阱this.update() 只能在组件方法中使用,不能在普通回调函数中使用。这在嵌套组件中尤其容易出错。

  3. AbortController 的全局状态管理:每个需要取消异步请求的组件都需要管理自己的 AbortController 实例。当组件卸载时,如果还有未完成的请求,必须主动调用 abort()

  4. Remix 3 的错误边界在路由层:每个路由文件可以导出一个 ErrorBoundary,但在 Frame 中使用需要特殊处理,因为 Frame 是独立的渲染上下文。

  5. Session/Cookie 管理的迁移remix-utils 库中提供的 createCookieSessionStorage 等工具在 Remix 3 中仍可用,但需要使用 Web 标准 Headers 而非 Remix 特有的 createHeadersFromAppendList

7.3 部署相关(5条)

  1. Cloudflare Workers 的大小限制:Cloudflare Workers 免费套餐限制为 1MB,Pro 套餐为 10MB。Remix 3 的核心运行时约 11KB,很安全,但如果你的应用包含了大量服务端模板,仍需注意大小。

  2. Node.js 兼容性问题:Remix 3 的某些 API(如 Response.json())在旧版 Node.js(<18)中不可用。部署到 Node.js 环境时,确保使用 Node.js 18+ 或 Bun。

  3. Deno Deploy 的特殊处理:Deno Deploy 对某些 Web 标准 API 的实现与浏览器略有不同,特别是 Request 的某些构造方法。部署前需要仔细测试。

  4. 数据库连接池:Remix 3 在边缘运行时上运行,每个请求可能来自不同的边缘节点。不要在请求外部创建数据库连接(这是常见的反面模式)。使用 Drizzle ORM 的连接池或使用 PlanetScale/Turso 这样的边缘数据库。

  5. 构建产物结构变化:Remix 3 的构建产物不再区分 serverclient。构建产物是一个可以在任何支持 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年的前端框架战争,才刚刚开始。


参考来源

  1. Remix 3 官方公告与 v3.0.0-beta.5 Release Notes
  2. Michael Jackson "Remix 3: The Future of Web Frameworks" 技术分享
  3. Cloudflare Workers 边缘运行时性能基准测试
  4. Shopify Hydrogen 框架与 Remix 集成文档
  5. Web Standards — Fetch API, Web Response, AbortController 规范文档

推荐文章

Mysql允许外网访问详细流程
2024-11-17 05:03:26 +0800 CST
程序员茄子在线接单