Remix 和 Next.js 怎么选?这两年隔一阵就会被人问一次。每次我都不急着说结论,而是反问一句:你的应用到底更依赖缓存,还是更依赖交互?这两个框架虽然都把自己定位成“React 全栈解决方案”,都能做 SSR、都能接管路由和数据获取,但骨子里的设计哲学完全不是一路人。搞懂 remix框架和 next.js框架有什么不同,本质上不是做参数对比,而是弄明白它们各自为谁、为什么这么设计。
如果你正准备技术选型、从零起一个新项目,或者单纯想弄清楚社区里的争论,这篇文章值得从头看。我不会只讲“哪个好”,我会先拆设计的出发点,再用同一个博客列表页分别跑一遍,最后把项目迁移时踩过的几个坑也列出来。看下来,你大概率能形成自己的判断。
1. 设计理念:一个像“水到渠成”,一个像“推倒重来”
开始对比之前,建议先把框架的底层设计目标放回桌面上。前端框架的差异,表面看是 API 和写法不同,深一层其实是它们对“Web 应用应该怎么构建”这件事的回答完全不同。
1.1 Remix:对 Web 标准近乎偏执
Remix 的设计目标不是“更聪明的 React”,而是“让开发者把 Web 本身的能力用好”。它把 HTML 表单、Cookie、请求头、缓存头这些原汁原味的标准能力直接暴露在路由层。在 Remix 里,每个路由文件可以导出 loader 和 action:loader 负责 GET 请求时的取数,action 负责表单 POST、PUT、DELETE 的变更逻辑。组件只是消费数据的视图层。
为什么这么设计?因为 Remix 的路由生态背景非常强调服务端渲染和多页面体验。团队希望数据获取尽可能发生在靠近网络的地方,让浏览器和 CDN 都能参与缓存。于是你会看到 Remix 项目里随手就能设置 Cache-Control 响应头,普通 a 标签跳转也能触发预加载。这种设计让它的学习曲线更贴近老派 Web 开发者的直觉:先有请求和响应,再有 React 界面。
1.2 Next.js:把 React 变成全栈运行时
Next.js 是从另一个角度切入的:既然团队已经在用 React 写界面,那为什么不把服务端能力直接装进 React 组件本身?它在较新大版本里的 App Router 基于 React Server Components,数据获取变成了组件内的一段异步逻辑,在服务端执行完直接给客户端渲染。这种思路让业务代码看起来非常紧凑:一个页面文件里既能读数据库,又能渲染 UI。
代价是框架引入了一套自己的运行时概念:服务端组件、客户端组件、序列化边界、Suspense 流式渲染,以及多级缓存。设计目标偏“平台化”:框架像造好一座底座,部署到对应的托管平台上,缓存、渲染、边缘逻辑都会被自动安排。体感上,Next.js 更像一个把 React 变成“全栈运行时”的大一统方案,从开发到部署都愿意替你接管。
1.3 两种哲学带来的体感差异
把两者放在同一张表里看,第一层差异其实非常清楚:
| 对比维度 | Remix | Next.js |
|---|---|---|
| 核心思路 | 用好 Web 标准,让路由承载数据与变更 | 把 React 推到服务端,组件即全栈 |
| 开发者心智 | 请求、响应、表单、缓存头 | RSC、序列化边界、运行时缓存 |
| 适合的团队 | 熟悉传统后端渲染、看重可控性的团队 | 愿意接受平台化约束、追求一体化的团队 |
| 数据获取入口 | loader / action | 组件内异步逻辑 + Server Actions |
我不评价谁更高明,但有一点几乎是共识:Remix 让“懂 Web 的人”上手更快,Next.js 让“懂 React 组件模型的人”感觉更顺手。理解了这一层,后面的路由、缓存、部署差异才会有落脚点。
2. 路由与数据加载:最核心的分水岭
路由是这两个框架差异最集中的地方。很多人一开始只以为一个用文件夹一个用文件,实际上背后的数据加载方式完全不同。
2.1 Remix:loader 是路由的“后端半边”
Remix 的项目里,所有页面都是文件式路由:app/routes/posts.tsx对应/posts,app/routes/posts.$id.tsx对应/posts/:id。当用户访问/posts/123时,框架会同时收集/、/posts、/posts/123三层路由的 loader,并行执行,结果一起交给组件渲染。
关键点是“并行和嵌套”。每个路由的 loader 只负责自己那一层的数据,父子路由互不阻塞。比如一个典型的帖子详情页可以这样写:
// app/routes/posts.$id.tsx import { json } from "@remix-run/node"; import { useLoaderData } from "@remix-run/react"; export async function loader({ params }: { params: { id: string } }) { const post = await getPost(params.id); // 从数据库或内部 API 取数 return json({ post }); } export default function Post() { const { post } = useLoaderData<typeof loader>(); return <h1>{post.title}</h1>; }组件本身不负责取数,它只从useLoaderData()里消费结果。如果你想在页面上展示一个动态的“更新中”状态,Remix 还提供了useNavigation这类 hook,能拿到整个页面跳转的加载状态。这一点对中后台系统特别有用:父布局不会因为子路由切换而重新刷数据。
2.2 Next.js:在服务端组件里直接 await
Next.js 的 App Router 没有显式的 loader。一个app/posts/[id]/page.tsx文件里可以直接这样写:
// app/posts/[id]/page.tsx export default async function PostPage({ params }: { params: { id: string } }) { const post = await getPost(params.id); return <h1>{post.title}</h1>; }这里的getPost可以直接连接数据库,不需要显式声明“这是一个 loader”。因为页面组件本身是异步服务端组件,await 之后拿到的数据直接参与渲染。这个设计让路由文件变得非常扁平,也把“数据获取”和“UI 渲染”压缩到了同一段代码里。
但这也就带来一个实际问题:你很难像 Remix 那样清楚地区分“当前页面需要哪些数据”。在 Remix 里,loader 是一个路由的显式契约,服务端执行完再统一交给组件;在 Next.js 里,只要某个服务端组件里出现了 await,它就在做数据获取。业务一旦复杂,RSC 的树层级需要你刻意去管理,不然数据请求很容易散落在各个组件深处。
2.3 嵌套路由:父层级的保活与并行
两个框架都支持嵌套布局,但机制不一样。
Remix 的路由嵌套是文件系统层级直接映射 UI 嵌套。比如/settings下有父布局和多个子路由,切换子路由时父布局组件不会重新加载,父子路由的 loader 可以并行执行。这对侧边栏、权限菜单、工作台这类场景非常友好。
Next.js 的layout.js也能让父布局在子页面切换时保持挂载,但它没有“按路由统一收集再并行”的机制。父布局和子页面各自内部的 RSC 数据获取,主要靠 React 的并发调度和框架缓存来协调。如果你在父布局里 fetch 了用户信息,在子页面里 fetch 了列表数据,它们之间的加载状态是分离的,需要自己用 Suspense 做统一流式体验。
实操体感上:中后台场景里,Remix 的嵌套加载模型更可预期;营销页和内容站场景里,Next.js 的流式渲染让首屏可以逐块呈现,体验也很顺手。
3. 表单、变更与缓存:实际开发的细节差距
路由和数据加载的差异是底层的,表单与缓存才是日常开发里每天都要碰的事。这里我必须多说几句,因为不少项目迁移时的痛点都集中在这一层。
3.1 Remix 的表单:回到浏览器原生能力
Remix 中一个<Form method="post">默认不是调用 API,而是把表单作为一个标准的 POST 请求发送到当前路由的 action。请求进入 action 之后,可以用request.formData()拿到字段。action 返回错误或成功时,框架会自动重新验证所有相关 loader,页面数据立刻更新。
// app/routes/posts.tsx export async function action({ request }: { request: Request }) { const formData = await request.formData(); const title = String(formData.get("title") || ""); if (!title.trim()) { return json({ error: "标题不能为空" }, { status: 400 }); } // 这里写入数据库 return json({ ok: true }); }这个模型最大的价值是渐进增强:就算某个页面 JS 加载失败,原生<form>也能按浏览器自带行为提交,action 照样执行,页面照常更新。我在一个内部后台项目里专门试过把 JS 全部停掉,表单依然能新增一条记录。这种稳定感在内部运营系统里是很实际的加分项。
另外,如果你只想更新某一块区域而不希望整个页面刷新,可以用useFetcher。它能在当前页面不动的情况下提交到任意路由的 action,并且支持乐观 UI。底层仍然是标准的请求/响应循环,所以调试起来很直接。
3.2 Next.js Server Actions:把服务端函数塞进组件
Next.js 新一代的变更逻辑由 Server Actions 承载。用法是在一个函数上标"use server",放进<form action={fn}>,浏览器会把表单数据序列化后传给服务端执行这个函数。执行完后,你需要手动调用revalidatePath或revalidateTag来刷新数据,否则页面可能还是旧内容。
React 19 之后配合useActionState,可以在客户端组件里拿到提交状态和错误信息。但要说明的是:Server Action 更像一个 RPC 函数,不像 Remix 那样天然带着“请求-响应”的完整上下文。在 action 里拿请求头、设置 cookie 虽然能做,但写法上比 Remix 多绕一层。
安全方面也要注意:Server Action 本质上是暴露给客户端的服务端端点,只写一个"use server"并不代表安全。我见过不止一个新人把删除操作直接绑在按钮上,完全没有校验当前用户是否有权限。这一点和传统 API 路由一样,必须自己做授权和来源校验。
3.3 缓存模型的根本差异
这是我认为选型时最需要重点评估的一项。
Remix 很少主动缓存页面 JSON。它处理的是标准的 HTTP 响应:你可以在 loader 里设置Cache-Control头,CDN 或浏览器按标准规则缓存;要让缓存失效,就通过 URL 或响应头控制。模型简单,也足够透明。
Next.js 的缓存体系要大得多:Full Route Cache、Data Cache、Router Cache、Server Function Cache,几层缓存互相影响,而且不同大版本的默认策略还变过多次。这就造成了一个很典型的坑:网上很多两年前的教程还在说某个接口默认缓存,新版本却已经默认不缓存了。我见过不止一个项目上线后,数据改了页面不刷新,排查半天发现是某个 fetch 请求走了指定缓存目录。
用一个表来概括这两者的缓存心智:
| 缓存相关操作 | Remix | Next.js |
|---|---|---|
| 页面数据刷新 | action 后自动 revalidate loader | 需要显式调用 revalidatePath |
| 缓存控制方式 | 标准 HTTP 头,理解成本低 | 框架多层缓存,需要逐层理解 |
| 静态优化路径 | 构建时可整体静态导出 | 支持静态导出但限制较多 |
| 调试入口 | 浏览器开发者工具里的网络面板 | 需要同时看框架日志和网络面板 |
如果团队对 HTTP 缓存机制更熟,Remix 更容易在产品里做精细控制;如果更愿意接受“配置完交给托管平台”,Next.js 的平台化缓存可以省下不少运维精力。
4. 同一个页面,两个框架分别怎么落地
前面讲了那么多理论,不如拿一个最简单也最有代表性的场景过一遍:帖子列表页 + 新增帖子表单。这个场景同时包含“读数据”和“写数据”,正好覆盖路由、组件、数据变更三个环节。
4.1 Remix 版本:一个文件搞定列表与提交
Remix 的写法和它整个框架的路由模型保持一致,一个app/routes/posts.tsx文件里同时出现 loader、action、组件三部分:
// app/routes/posts.tsx import { json } from "@remix-run/node"; import { useLoaderData, Form, useActionData } from "@remix-run/react"; export async function loader() { const posts = await getPostsFromDb(); return json({ posts }); } export async function action({ request }: { request: Request }) { const formData = await request.formData(); const title = String(formData.get("title") || ""); if (!title.trim()) { return json({ error: "标题不能为空" }, { status: 400 }); } await createPost(title); return json({ ok: true }); } export default function Posts() { const { posts } = useLoaderData<typeof loader>(); const actionData = useActionData<typeof action>(); return ( <div> <ul> {posts.map((post) => ( <li key={post.id}>{post.title}</li> ))} </ul> <Form method="post"> <input name="title" /> {actionData?.error && <p>{actionData.error}</p>} <button type="submit">新增</button> </Form> </div> ); }这里最值得强调的是“默认重新验证”。当 action 执行成功之后,Remix 会自动重新执行 loader,页面上的列表立刻刷新,不需要额外调用任何 API。你写的是一个标准 HTML 表单,框架替你处理了后续的数据同步。
4.2 Next.js 版本:服务端组件加客户端表单
Next.js 的情况稍微复杂一点。为了能展示提交错误,通常要把 Server Action 放进单独文件,再用一个客户端组件包一下表单。服务端组件只负责读取和渲染列表。
第一个文件是 Server Action:
// app/posts/actions.ts "use server"; import { revalidatePath } from "next/cache"; export async function createPost( prevState: { error?: string }, formData: FormData ) { const title = String(formData.get("title") || ""); if (!title.trim()) { return { error: "标题不能为空" }; } await createPostInDb(title); revalidatePath("/posts"); return { ok: true }; }第二个文件是客户端表单组件:
// app/posts/post-form.tsx "use client"; import { useActionState } from "react"; import { createPost } from "./actions"; export function PostForm() { const [state, action] = useActionState(createPost, {}); return ( <form action={action}> <input name="title" /> {state.error && <p>{state.error}</p>} <button type="submit">新增</button> </form> ); }第三个文件是页面的服务端组件:
// app/posts/page.tsx import { PostForm } from "./post-form"; export default async function PostsPage() { const posts = await getPostsFromDb(); return ( <div> <ul> {posts.map((post) => ( <li key={post.id}>{post.title}</li> ))} </ul> <PostForm /> </div> ); }4.3 从代码对比里能看出什么
两个版本都能完成“列表 + 新增”这个功能,但结构和心智很不一样。
Remix 把数据读取、数据变更和 UI 放在一个路由文件里,意图非常集中。写的时候你会觉得“这就是服务端开发加上 React 视图层”。Next.js 则把 Server Action、客户端表单、服务端页面拆成了三个文件,边界更清晰,但第一次接触的人很容易搞混:为什么一个表单要把逻辑拆到三个地方?
另外一个体感差异是错误处理。Remix 的 action 返回错误后,组件可以直接从useActionData里拿;Next.js 需要useActionState配合客户端组件,还要注意"use client"和"use server"的导入方向。不是说它不好,而是它把一套更模块化的工程结构放到了你面前,代价是学习门槛更高。
5. 部署、生态与团队适配
框架选型最终要落到部署环境和团队能力上。一个框架在 Demo 里再顺手,如果和现有运维链路冲突,上线成本会成倍上升。
5.1 Remix 适配器模式:一台 Node 服务器就够了
Remix 通过适配器接入不同运行时:@remix-run/node跑普通 Node 服务器,其他适配器还能跑边缘运行时。构建产物是常规的服务器 bundle 加一份静态资源目录,放到任何支持 Node 的容器或虚拟机上都能跑。
我在传统服务器上部署过 Remix 应用,做法很简单:构建完把产物丢上去,用进程守护工具拉起服务,再配一个反向代理。没有额外依赖什么特殊平台能力。如果不想跑服务器,Remix 也支持纯静态导出,把构建结果交给静态托管即可。这个灵活度在不少受限环境里非常关键。
5.2 Next.js 的托管优势与自托管成本
Next.js 在商业托管平台上确实能发挥最大作用:缓存 API、图片优化、边缘函数都可以一站式解决。但如果你只能在通用服务器上自托管,情况就会复杂一些。比如某些版本里的静态优化和动态渲染并存时,需要确认缓存目录能持久化;多实例部署时,框架层的缓存如何保持一致,也需要单独规划。
还有一点是版本迭代速度。Next.js 的大版本变化快,API 和默认行为的调整在升级时会带来迁移成本。我见过一个项目升级到新大版本后,原本正常的路由缓存策略变了,需要逐个路由重新验证。相比之下,Remix 的版本节奏更克制,很多写法从一个版本带到下一个版本基本不用改。
5.3 什么场景我分别推荐
我给团队做选型建议时,通常按这几种情况判断:
- 中后台系统、重表单、多层级权限:优先考虑 Remix。嵌套路由和 action 模型写起来顺,父布局不会随意刷新,表单提交天然具备渐进增强能力。
- 内容站、文档站、活动页:两个都能用。Next.js 的流式渲染和静态优化很成熟,Remix 的缓存头控制也很简单,关键看团队更习惯哪套模型。
- 大型多交互产品,且具备商业托管条件:Next.js 的 RSC 生态和一体化体验更有优势。
- 内网部署、限制多、只能用普通 Node 进程:Remix 的适配器模型更省心。
6. 常见问题与避坑实录
最后整理几个实际项目里最容易遇到的坑。这些东西官方文档不一定细写,但踩过一次之后印象会非常深。
6.1 用 Remix 常见的坑
第一个是容易把客户端状态塞进 loader。Remix 的 loader 服务端执行,拿不到浏览器状态。如果某个数据完全来自用户当前操作,应该用useFetcher或客户端数据加载,而不是硬塞进 loader。
第二个是静态导出和动态数据的冲突。Remix 支持静态导出,但如果你在 loader 里依赖 session 或请求头,那这个路由就无法静态化。上生产前要把哪些路由能静态化、哪些必须跑服务器提前理清楚。
第三个是版本迭代带来的配置差异。新版 Remix 用 Vite 做构建链路,早期教程里基于 Webpack 的配置在升级后可能不适用。遇到构建报错,先确认你用的版本和教程年份是否匹配。
6.2 用 Next.js 常见的坑
第一个是 RSC 边界混乱。在服务端组件里直接使用useState或事件处理函数会报错,解决方法是拆成客户端组件。但不少项目为了省事,在文件顶部盲目加"use client",把本来应该留在服务端的逻辑全部拖到了客户端。
第二个是缓存不更新。修改数据后忘记调用revalidatePath或revalidateTag,页面就一直显示旧数据。排查这类问题,我建议先确认“这次请求到底走的是哪一层缓存”,再看代码逻辑。多看几层之后,你会发现大部分问题不是代码写错,而是缓存策略没对齐。
第三个是自托管时的平台能力差异。有些功能在商业托管平台上开箱即用,自托管后行为却不一样。比如图片优化在自托管环境里需要额外配置,中间件运行环境对第三方库也有兼容要求。部署前最好做一轮完整的冒烟测试,别只测完本地就跑。
6.3 从一边迁到另一边,先改什么
如果团队决定从 Remix 迁到 Next.js,或者反过来,我建议先不要在代码层面大动,而是把一个代表性页面完整跑通三件套:路由、数据读取、表格提交。
路由映射比较容易:Remix 的routes/posts.tsx对应 Next.js 的app/posts/page.tsx,布局文件对应类似位置。数据读取差异较大,Remix 的 loader 要改成 RSC 里的异步函数或fetch,Next.js 的 RSC 取值逻辑也可以反过来改写成 Remix 的 loader。表单提交变化最明显,Remix 的 action 对应 Next.js 的 Server Action,但缓存刷新机制完全不同,需要重新设计。
还有一个容易被忽略的点:第三方库的适配情况。有些以客户端组件为中心的库迁移到 RSC 页面后工作正常,有些则必须额外包一层客户端组件才能不出错。迁移前把这些依赖梳理一遍,能帮你提前避开不少隐藏问题。
我个人体会是,这两个框架没有绝对的优劣,更多是“谁更符合当前团队和业务的状态”。如果你做一个内部后台,组员大部分时间里在写表单和状态交互,Remix 的模型会让你少想很多事;如果你做一个面向内容、快速上线的项目,团队也愿意接受平台化托管,Next.js 的工程一体化会带来不少便利。最后送一个小技巧:与其看十篇对比帖,不如用同一个数据模型分别写一个带表单的列表 Demo,重点看两件事——路由嵌套时父页面是否被反复刷新、提交后数据更新要不要手动处理。实测一轮,你基本就能得到自己的答案。