与 Next.js 应用路由器共度的岁月——为何我们选择转型
我持续使用Next.js进行专业开发,但其核心设计的应用路由器和React服务器组件 (RSC)令我深感挫败。问题并非出在小漏洞或API混乱上,而是对Vercel和React团队在构建过程中做出的根本性设计决策存在重大分歧
对 React 服务器组件与 Next.js 15 的批判性思考
在为雇主开发网页应用时,我持续使用Next.js进行专业开发,但其核心设计的应用路由器和React服务器组件 (RSC)令我深感挫败。问题并非出在小漏洞或API混乱上,而是对Vercel和React团队在构建过程中做出的根本性设计决策存在重大分歧。
参加的Web开发活动越多,越发现许多人虽厌恶Next.js却仍被迫使用。本文将分享我和同事如何逃离这个地狱,将整个前端无缝迁移至TanStack Start。
目录:
技术解析:什么是服务器组件?§
RSC的核心理念是将组件划分为两类: “服务器” 组件与 “客户端” 组件。服务器组件不包含useState、useEffect等功能,但可采用async function形式,并能直接调用数据库等后端工具。客户端组件延续现有模式:后端代码生成HTML文本,前端代码通过window.document.*管理DOM。
首个灾难:命名混乱!!React现在用 “服务器” 和 “客户端” 指代特定概念,完全无视其既有定义。问题在于—— 客户端 组件同样能在后端运行!本文将采用 “后端” 和 “前端” 术语描述Web应用的两大运行环境:分别对应Node.js进程与Web浏览器。
这种 服务器/客户端 组件模型颇具巧思。由于<Suspense />等内置组件会经网络序列化传输,数据获取可通过异步 服务器组件 实现极简建模,而回退界面则呈现出客户端般的交互体验。
src/app/\[username\]/page.tsx
// For this article, server components will be highlighted in red
export default async function Page({ params }) {
// Page params are given as a resolved promise
const { username } = await params;
// The components `UserInfo` and `UserPostList` will be run at the same
// time. Once `UserInfo` is ready, the visitor will see the page with a
// `PostListSkeleton` if the post list is not yet ready.
return <main>
<UserInfo username={username} />
<Suspense fallback={<PostListSkeleton />}>
<UserPostList username={username} />
</Suspense>
</main>
}
// Waterfalls are avoided by having multiple components, which
// are all evaluated at the same time.
async function UserInfo({ username }) {
const user = await fetchUserInfo(username);
return <>
<h1>{user.displayName}</h1>
{user.bio ? <Markdown content={user.bio} /> : ""}
</>
}
async function UserPostList({ username }) {
const posts = await fetchUserPostList(username);
return /* post list ui omitted for brevity */;
}
若忽略 React 本身 40kB 的 gzip 打包大小,上述示例中 UI 与数据获取完全无需 JavaScript 代码——它仅需流式传输标记!例如,<Markdown />组件内部的假想Markdown解析器始终驻留在后端。当需要交互式前端时,只需在以“use client”开头的文件中创建 客户端组件 即可。
src/components/CopyButton.tsx
"use client"; // This comment marks the file for client-side bundling.
export function CopyButton({ url }) {
return <>
<span>{url}</span>
<button onClick={() => {
const full = new URL(url, location.href);
navigator.clipboard.writeText(full.href);
// omitting error handling, success ui, styles
}}>copy</button>
</>
}
src/app/q+a/Card.tsx
export function Card() {
return <article>
<header>
{/* Make the browser import the copy button */}
<CopyButton url="/q+a/2506010139" />
</header>
<p>
{/* Process markdown on the backend */}
<Markdown content=".........." />
</p>
</article>
}
应用路由器的实际陷阱§
在退出Bun运行时工程师岗位后(我曾实现服务器组件打包和RSC模板), 我加入了一家小型公司,参与前端开发:基于Next.js构建的应用程序,后端采用Hono框架。以下笔记是我在维护和开发新功能时,针对实际问题进行的简化总结。所有这些问题导致团队成员的时间被浪费在两件事上:要么绕过设计缺陷,要么互相解释为何本应无关紧要的问题却成了无法撼动的障碍。
乐观更新不可行§
Next.js 文档在说明执行突变操作时未提及乐观更新;似乎未考虑此场景。由 React服务器 渲染的组件,按设计在挂载后不可修改。可能变更的元素需置于客户端组件中,但客户端组件无法进行数据获取——即使在后端SSR过程中亦然。这导致服务器端组件功能极其有限,仅负责数据获取,而客户端组件则包含页面的大部分静态内容。
src/app/user/\[username\]/page.tsx
export default async function Page() {
const user = await fetchUserInfo(username);
return <ProfileLayout>
<UserProfile user={user} />
</ProfileLayout>;
}
src/app/user/\[username\]/UserProfile.tsx
"use client"; // Must separate the client code into a second file!
export function UserProfile({ user: initialUser }) {
// There are many great state management libraries out there;
// for simplicity, this example will use one state cell.
const [user, optimisticUpdateUser] = useState(initialUser);
async function onEdit(newUser) {
optimisticUpdateUser(newUser);
const resp = await fetch("...", {
method: 'POST',
body: JSON.stringify(newUser),
... // (headers, credentials, tracing, and more)
})
if (!resp.ok) /* always remember to test for errors! */
}
return <main>{/* user interface with editable fields... */}</main>:
}
随着页面交互需求增加,试图将静态部分完全保留在服务器端变得越来越混乱。在工作应用中,几乎每个 UI 元素都会显示动态数据。通过 WebSocket 可实时同步更新数据(例如用户卡片中的在线状态及其基本资料)。由于此类组件架构难以让工程师理解和维护,我们几乎所有页面都完全采用“use client”模式,通过page.tsx定义数据获取逻辑。
以下是实际应用中更具体的示例,展示我们工作中使用的数据获取库TanStack Query。
src/queries/users.ts
// At work, there is a helper function `defineQuery` for type safety.
// Fetchers are trivial and can run on the backend or the frontend.
export const queryUserInfo = (username) => ({
queryKey: ['user', username],
queryFn: async ({ ... }) => /* fetch data */
});
src/app/user/\[username\]/page.tsx
export default async function Page({ params }) {
const { username } = await params;
// There's no global state in the React Server. Since layouts
// are executed in parallel, the TanStack `QueryClient` has to
// be reconstructed multiple times per route.
const queryClient = new QueryClient();
await queryClient.ensureQueryData(queryUserInfo(username));
// HydrationBoundary is a client component that passes JSON
// data from the React server to the client component.
return <HydrationBoundary state={dehydrate(queryClient)}>
<ClientPage />
</HydrationBoundary>;
}
src/app/user/\[username\]/ClientPage.tsx
"use client";
export function ClientPage() {
const { username } = useParams();
const { data: user } = useSuspenseQuery(queryUserInfo(username));
// ... some hooks
return <main>
{/* ... an interactive web page */}
</main>;
}
此示例必须拆分为三个独立文件,这是由于服务器端组件打包规则所致(客户端组件需要“use client”声明,且服务器端组件文件常因仅限服务端的导入语句而无法在客户端被引入)。若采用Pages路由方案,由于getStaticProps和getServerSideProps具备树摇功能,本可整合为单一文件。
每次导航都是另一次数据获取§
由于应用路由器将每个页面初始化为服务器端组件(理想情况下仅包含小范围交互区域),因此跳转至新页面时,无论客户端已拥有何种数据,都必须重新从Next.js服务器获取内容!即使存在loading.tsx文件,当用户打开/页面、跳转至/other再返回/时,系统仍会显示加载状态并重新获取首页内容。
唯一例外是 纯静态内容 场景,此时即时跳转和预加载效果极佳。但 Web应用并非静态 ,它们包含大量动态内容。登录状态会影响首页显示——这种情况令人抓狂,因为客户端明明拥有即时呈现页面的全部资源,却因cookie未更新而无法刷新。
附注 :在空白项目进一步测试中,我观察到Next前端代码会预加载路由,但 完全不包含实际内容 。在Hello World示例中,这段1.8KB的RSC有效负载竟指向两个不同的JS代码块,且重复调用四次。这纯粹是带宽和出站流量的浪费——尤其考虑到点击链接时所有数据都会被重新加载。
1:"$Sreact.fragment" 2:I[39756,["/_next/static/chunks/ff1a16fafef87110.js","/_next/static/chunks/7dd66bdf8a7e5707.js"],"default"] 3:I[37457,["/_next/static/chunks/ff1a16fafef87110.js","/_next/static/chunks/7dd66bdf8a7e5707.js"],"default"] 4:I[97367,["/_next/static/chunks/ff1a16fafef87110.js","/_next/static/chunks/7dd66bdf8a7e5707.js"],"ViewportBoundary"] 6:I[97367,["/_next/static/chunks/ff1a16fafef87110.js","/_next/static/chunks/7dd66bdf8a7e5707.js"],"MetadataBoundary"] 7:"$Sreact.suspense" 0:{"b":"TdwnOXsfOJapNex_HjHGt","f":[["children","other",["other",{"children":["__PAGE__",{}]}],["other",["$","$1","c",{"children":[null,["$","$L2",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L3",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","forbidden":"$undefined","unauthorized":"$undefined"}]]}],{"children":null},[["$","div","l",{"children":"loading..."}],[],[]],false],["$","$1","h",{"children":[null,["$","$1","KCFxAJdIDH3BlYXAHsbcVv",{"children":[["$","$L4",null,{"children":"$L5"}],["$","meta",null,{"name":"next-size-adjust","content":""}]]}],["$","$L6","KCFxAJdIDH3BlYXAHsbcVm",{"children":["$","div",null,{"hidden":true,"children":["$","$7",null,{"fallback":null,"children":"$L8"}]}]}]]}],false]],"S":false} 5:[["$","meta","0",{"charSet":"utf-8"}],["$","meta","1",{"name":"viewport","content":"width=device-width, initial-scale=1"}]] 9:I[27201,["/_next/static/chunks/ff1a16fafef87110.js","/_next/static/chunks/7dd66bdf8a7e5707.js"],"IconMark"] 8:[["$","title","0",{"children":"Create Next App"}],["$","meta","1",{"name":"description","content":"Generated by create next app"}],["$","link","2",{"rel":"icon","href":"/favicon.ico?favicon.0b3bf435.ico","sizes":"256x256","type":"image/x-icon"}],["$","$L9","3",{}]]经审查发现其中实际存在内容:加载状态。你注意到了吗?
["$","div","l",{"children":"loading..."}]这种浪费依然严重,因为所有数据都会在实际页面的RSC中重新输出。
解决方法似乎是使用staleTime,但该功能标记为实验性且“不推荐用于生产环境”。这种非默认的补丁式配置选项实在令人尴尬。即便启用该功能,也无法让多个引用相同底层数据的页面共享数据。
App Router无法处理的加载状态场景是:当用户在类似Git项目问题页面的界面点击用户名跳转至个人主页时。使用loading.tsx时整个页面仅为骨架结构,但若通过TanStack Query建模此类查询,即可在用户资料和仓库数据加载期间即时显示用户名和头像。服务器端组件无法支持此类导航,因为数据仅存在于渲染组件中,故需重新获取。
在我们的Next.js站点中,服务器端数据获取器添加了以下代码行,通过跳过数据获取阶段来加速软导航:
src/util/tanstack-query-helpers.server.ts
export function serverSidePrefetchQueries(queries) {
if ((await headers()).get("next-url")) {
// This is a soft-navigation. SKIP the prefetching to make it faster.
// The client might already have this data, and if not, they have the
// loading state. Ideally, this server request wouldn't exist -- The
// client side has nearly ALL the code since the app is written mostly
// as client components. Kind of a design flaw of the App router TBH.
return;
}
// ... data prefetching-logic ...
}
此外,loading.tsx 文件应包含 useQuery 调用,这样在加载空 RSC 的网络请求期间,若实际需要数据,系统会同步进行数据获取。实际上,loading.tsx 的状态可以直接作为客户端组件本身,此时您将看到客户端页面。
src/app/user/\[username\]/loading.tsx
"use client";
export default function PageLoadingSkeleton() {
return <ClientPage />;
}
在工作中,我们仅让
loading.tsx文件包含useQuery调用并显示骨架界面。这是因为当 Next.js 加载实际服务器组件时,无论如何都会导致整个页面重新挂载。此时不会进行虚拟 DOM 差异比较,意味着所有钩子(useState)在请求完成后都会略微重置。我曾尝试复现一个简单场景,迫使 Next.js 仅更新现有 DOM 并保留状态,但始终未能实现。所幸空白 RSC 调用的耗时足够短暂。
布局的人为限制§
布局组件虽能执行数据获取,却无法以任何形式观察或修改请求。此设计使Next.js能随时获取并缓存布局组件。而在其他框架中,布局组件与页面组件并无功能差异,本质上都是普通组件。
独立获取布局组件的构想虽巧妙,却因其意味着每次布局加载都需重新获取数据而显得荒谬。你无法共享QueryClient,只能依赖其猴子补丁化的fetch来实现承诺的相同GET请求缓存。
当同事问我为何Next.js会拒绝某些代码时,我已放弃解释技术细节,直接说:“这是Next.js的技能问题,我很快就会搞定它,别担心。”这些规则对普通开发者而言实在难以理解。
你依然会重复下载所有内容§
与“Islands Architecture”不同,服务器组件仍需在前端进行水化处理以支持Suspense并保留客户端组件状态。进行软导航时,“RSC有效负载”(完全不属于HTML)会通过fetch获取。在新页面加载时,HTML 用于实现 首次绘制,但该 HTML 不包含客户端组件和 Suspense 的相关信息。React 的解决方案是 发送整个页面的标记代码副本 。Next.js 生产服务器在动态页面渲染时发送的内容示例如下:
GET /user/clover
<!DOCTYPE html>
<html>
<head>
{link and meta tags}
</head>
<body>
{server side render}
<script>
// a bootstrap script that sets up global `__next_f` as
// an array. once React loads, this `.push` function
// gets overwritten to write new chunks directly to the
// RSC decoder. this script has some dom helpers too
(self.__next_f=self.__next_f||[]).push([0])
</script>
<script>
// the RSC payload for the application shell.
self.__next_f.push([1,"1:\"$Sreact.fragment\"\n2:I[658993,[\"/_next/st{...}"])
</script>
<!--
the closing </body> is NOT written yet, since there is a
suspense boundary not resolved. time passes, and only
then is more data is written
-->
<div class="user-post-list">
{server side render of a Suspense boundary}
</div>
<script>
// the RSC payload for the suspense boundary
self.__next_f.push([2,"14:[\"$\",\"div\",null,{\"children\":[[\"$\",\"h4\"{...}"])
</script>
<!-- HTML and script tags repeat until the entire page is done -->
</body>
</html>
该方案导致 初始HTML负载体积翻倍 。更糟的是,RSC负载包含用JS字符串字面量引用的JSON数据,这种格式远不如HTML高效。虽然brotli压缩效果良好且浏览器渲染速度快,但这种做法仍属资源浪费。采用数据注入模式时,本地数据至少可复用于交互操作及其他页面。
即便在交互性极低甚至无交互的页面中,这种方案仍会造成资源消耗。以Next.js文档为例,加载其主页时,页面大小约达750kB(其中HTML占250kB,脚本标签占500kB),且内容重复出现两次。
您可在Mac上按Cmd + Opt + u(其他平台按Ctrl + u)验证此现象,随后使用Cmd/Ctrl + f查找博客内容(如“构建全栈Web应用”),会发现该内容重复出现两次。由于这是React服务器组件的核心特性, 这种浪费无法避免 。
这种RSC格式确实存在更多冗余。但我实在不想深究为何字符串/_next/static/chunks/6192a3719cda7dcc.js会出现27次。搞什么鬼啊各位?你们带宽是白送的吗???
Turbopack 糟透了§
本节内容纯属吐槽。
- Turbopack 速度不快
- Turbopack 生成的代码在调试器中难以调试(开发模式下)
- Turbopack 在许多情况下抛出糟糕的错误信息
通常我不会为此单独开篇,但我想直接列举项目中的三个真实案例。
第一个案例发生在重构以满足服务器/客户端组件模型时,我误将一个客户端组件设为 async。这个错误特别恼人,因为它完全不指出问题所在,只包含了 服务器 的堆栈跟踪。

另一个糟糕的错误信息案例:

在修复第二个错误的根本问题后(具体原因已记不清),开发服务器卡死,必须重启才能恢复。
最后一个问题是,我设置调试器断点时,变量名hello总被替换成__TURBOPACK__imported__module__$5b$project$5d2f$client$2f$src$2f$utils$2f$filename$2e$ts__$5b$app$2d$client$5d$__$28$ecmascript$29$__["hello"]之类的狗屁东西。
好吧。这简直糟透了。我们能怎么办?
在工作中无缝弃用 Next.js 和 Vercel§
Web 项目主要分为两类:
- 以静态内容为主的网站
- 包含大量动态交互组件的Web应用
而Next.js对这两类场景都适用性欠佳。若您属于第一类静态网站,建议选用Astro或Fresh。对于需要 React 完整功能的开发者,本节将分享我如何逐步无缝地用 TanStack Start 替代供应商锁定的 Next。
最初使用的是这个 Vite 配置文件。
vite.config.ts
const config = defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), "NEXT_PUBLIC_");
return {
// Use the Next.js default port 3000
server: { port: 3000 },
// Use the Next.js default env prefix "NEXT_PUBLIC_"
define: Object.fromEntries(Object.entries(env).map(
([k, v]) => [`process.env.${k}`, JSON.stringify(v)])),
plugins: [
viteTsConfigPaths({ projects: ["./tsconfig.json"] }),
tailwindcss(),
// For ease of understanding from coworkers, I started porting
// the routes in `src/tanstack-routes`. When the migration was
// done, it would go back to the default `src/routes`.
tanstackStart({
router: { routesDirectory: "src/tanstack-routes" },
}),
viteReact(),
],
resolve: {
// The key to the incremental migration: redirect `next` elsewhere
alias: { next: path.resolve("./src/tanstack-next/") },
conditions: ["tanstack"],
extensions: [
// Allow a file named like `utils/session.tanstack.ts` to
// override `utils/session.ts` when imported.
".tanstack.tsx", ".tanstack.ts",
// Default import extensions
".mjs", ".js", ".mts", ".ts",
".jsx", ".tsx", ".json",
],
},
};
});
随后,我排查了所有Next.js API的使用场景,要么直接移除,要么为TanStack创建占位函数。例如src/tanstack-next/link.tsx实现了next/link:
src/tanstack-next/link.tsx
import { Link } from "@tanstack/react-router";
import type { LinkProps } from "next/link";
export default function LinkAdapter({ href, ...rest }: LinkProps) {
return <Link {...rest} to={href as unknown as any} />;
}
部分占位实现极其简单。最初我对
useRouter的实现仅为return {},后来才逐步为该对象添加方法。由于是临时方案,代码无需追求整洁。
现在,新站点几乎可以导入所有客户端组件——要么通过模拟所需的 Next.js API,要么借助 .tanstack.ts 扩展按文件逐个重构逻辑。不久后,我便让网站首页在 TanStack Start 上正常运行,并合并了该分支。

首个PR仅支持单个页面,通过添加千行代码并删除40行代码实现。此前我已提交过移除少量
next/image和next/font调用的补丁。
剩余工作是迁移所有其他路由。从Next.js迁移至其他框架时唯一损失的功能,是在UI中await数据获取函数的能力。实践中,将所有路由移入loader函数后,页面服务器端渲染时的行为变得清晰明了。对于需要多次数据获取的页面,可将这些请求合并为单次特殊API调用,一次性获取该页面的全部相关数据。
再次强调: 从服务器组件迁移的本质是简化代码——RSC天生会将你引向混乱的歧途 。网站几乎所有复杂模块都变得更易于工程师理解,唯一例外是团队需要适应新的文件系统路由规范。通过大量实例演示,我们最终都掌握了要领。
通过渐进式迁移,新代码不会破坏现有部署。TanStack逐步接管代码库,最终我们删除了所有Next.js占位符,获得了TanStack路由器提供的所有精美类型安全特性。最终网站在各维度都实现加速:开发模式响应更快、生产环境页面加载更迅捷、软导航更流畅,且成本低于Vercel托管的Next部署方案。
这种变化并非我们独有。尽管我尽量远离社交媒体,但有人发来了Brian Anglin在Superwall的工作成果,展示了TanStack Start实现的惊人CPU降幅。我还记得ChatGPT从Next.js迁移到Remix的讨论(随机网络闲谈: [1] [2] [3]) 约一年前。
next/metadata 非常出色§
在我看来,这是Next.js为数不多的优秀API之一,也是我们代码中迁移到TanStack后操作难度增加的唯一环节。为避免代码质量下降,我直接将他们的元数据API移植为常规函数,供所有人使用。最初我在NPM上实现了1:1移植,但今年初将其API简化为一个简洁易懂的文件。截至本文发布时,我已新增兼容 TanStack 的 meta.toTags API,可通过 JSR、NPM 安装,或直接复制到项目中使用。
注意 :由于撰写本文的时间限制,该库尚未更新。我可能会在本周结束前(10月24日)或近期完成更新… 作为临时替代方案,我可将工作环境使用的版本分享至个人网站:meta.tanstack.ts。
// once in your project
import * as meta from "@clo/lib/meta.ts";
export const defineHead = meta.toTags.bind(null, {
// site-wide options
base: new URL("https://paperclover.net"),
titleTemplate: (title) => [title, "paper clover"]
.filter(Boolean).join(' | '),
// ...
});
// for each page...
export const Route = createFileRoute("/blog")({
head: () =>
defineHead({
title: "clover's blog", // templated with `titleTemplate`
description: "a catgirl meows about her technology viewpoints",
canonical: "/blog", // joined with `base`
// When specified, configures Open Graph and Twitter embed,
// using the page title and description as the default.
// The defaults are good, but it supports more options.
embed: {
image: "/img/blog.webp",
},
// Every exotic meta tag is done with a JSX fragment. This
// doesn't render React, it just loops through the tags.
// My goal was to cover the most common 99% of uses.
extra: <>
<meta name="site-verification" content="waffles" />,
</>,
}),
component: Page,
});
function Page() {
...
}
我的版本并未试图覆盖Next.js元数据对象的全部功能,而是通过内联JSX来弥补这一空白。
next/og同样值得推荐§
没有强硬立场。只是想提醒大家 @vercel/og 包的存在。
我的体验如常§
在 Next.js Conf 2024 大会上,所有人都在热议服务器组件。具体和谁聊过记不清了,但行业大佬们都对此充满热情。而我作为 RSC 打包端的实现者,早已洞察到这种模式的若干问题。随着 Next 15 去年“稳定化”应用路由器,众多公司开始基于它构建产品,亲身体验到这些陷阱。
我接触Next.js较晚,直到今年六月才开始使用15版。但活动期间交流过的每位开发者都认同我的观点。在Bun’s 1.3派对上讨论此议题时,所有参与者都赞同我的看法。甚至Vercel的员工也坦言不喜欢Next.js的实际使用体验。
我期待TanStack Start稳定后,能成为众人期待的Next.js替代方案。
选择尊重开发者的工具§
JavaScript 生态中的许多东西都一团糟。正是这种混乱让网页开发饱受嘲讽。我曾多次认为网页开发是无法收拾的烂摊子,但实际上混乱只存在于我常用的库中。剥离这些外壳后,现代网页开发技术其实非常出色。
自2024年末起,我便在完全抛弃框架的前提下从零构建这个网站,亲手编写了诸如TUI进度控件、 静态文件代理、增量构建系统等组件。这些开发过程带给我数年间最愉悦的编程体验。 paper clover 的访客获得了更优质的网站体验;我创建的微型库被提取为公共资源,实现多方共赢。
这种从零开始的开发模式对多数人而言过于苛刻,尤其在工作场景中。我认为至少应当将精力与资金投入那些尊重开发者的优质工具。而Next.js及其背后的公司Vercel,显然不属于此列。
若您使用Next.js时也感受到这种不被尊重的体验,请思考您与同事是否愿意继续支持他们的无服务器帝国。目前Vite生态系统似乎相当可靠,但我尚未积累其工具在生产环境大规模应用的经验。Void0推出的Vite+颇具潜力,但这些风险投资支持的工具能否长期尊重我们(终端用户与开发者),唯有时间能给出答案。
截至本文撰写时,Next.js Conf 2025大会将于明日举行。与其购买800美元的门票,我决定将这笔钱捐给TanStack团队,以支持他们尊重并改善Web开发生态系统的行动。
未来展望
我正逐步用更优质的替代方案取代那些不尊重我的软件,例如GitHub、Visual Studio Code、DaVinci Resolve、Discord、Google Drive/Workspace等众多工具。我计划在此博客深入探讨技术实践(包括进展库开发、自建网站生成器的初衷、当前工作的经验总结),并分享在Bun公司的过往项目(详述热重载技术、崩溃报告器及内置模块打包的复杂系统)。
本文文字及图片出自 One Year with Next.js App Router — Why We're Moving On
自推出应用路由器以来,Vercel便接连做出令人费解的决策。它本可成为JavaScript领域的Rails,如今却沦为一团混乱的烂摊子:Turbopack缓存、中间件(现更名为“代理”)、荒谬的布局组件、RSC的拙劣实现,以及对所有功能都强推未完成的alpha版本。Next 充斥着令人眼花缭乱的缩写概念(RSC、SSR、PPR、SSG、ISG)。托管集成采用半专有方案,且频繁破坏 fetch 和重定向等基础 JS API。
更令人失望的是,他们始终未能提供应用必备的基础功能,比如国际化和身份验证。
无论何种情况,Next 都已不值得选择。
曾几何时,React专注于客户端领域——Redux虽略显复杂但只需掌握一次,React Router也不至于两天换一次规则。我们围绕这个技术栈(React+类、React Router、Redux)构建了卓越的企业级应用。那是我最后一次享受React。
如今其他一切都成了仓鼠转轮——徒劳奔跑却原地踏步。市场营销和金钱正主导着所有方向。或许某些领域正上演着“火与动”的战略博弈。
惯性机制虽有帮助,但收效甚微。
你依然可以这样构建网站。作为现公司的首任员工,我从首次提交起就主导了前端仓库的技术栈选择。只要避开NextJS,如今的技术栈比当年黄金时期更胜一筹。
我们采用tanstack-router、tanstack-query,底层使用vite。仅此而已。这些库比当年标准的redux/react-router/webpack更强大更简洁。
我让终生从事后端开发的同事们在这个代码库中保持着合理(且颇为愉快)的工作效率。
这套方案搭配了最高级别的TypeScript严格模式。组件库采用MUI,结合Playwright进行集成测试,使用Hoverfly模拟后端接口,并通过Open-API定义应用→BFF→后端之间的API交互。
这套技术栈配合claude-code,实现全项目自动化(即通过claude-code轻松运行所有组件)——堪称真正的生产力超级武器。复杂的业务应用(10-30页独立的中等复杂功能模块)可在一周内甚至更短时间内交付,而非耗费数周或数月。这套方案产出的代码库经过全面测试,具备生产环境部署能力,绝非原型级/临时代码。
Next.js不等同于React。若忽略服务器组件,纯React框架本身相当稳定。尽管围绕服务器组件的讨论甚嚣尘上,但它们完全可选,实际忽略起来相当容易。
PPR读来颇有意思。就像在法拉利问世十五年后,读到关于轮子再造的论述。
我实在无法想象,在网络传输中抛掷那些邪恶的混淆JS块^w代码块,怎么可能成为下一个Rails…
这根本不是重点。更无法想象有人会想用JSX之外的模板语言编写模板,可现实就是如此。
Rails及其同类框架的价值不在于单一技术选择,而在于提供完整且立场鲜明的框架,让开发者能构建符合单一思维模式的Web应用。
Rails如此,Django如此,Laravel亦然。而Next恰恰相反——其缓存机制等专属特性连单个开发者的思维都难以容纳,更遑论构建的应用本身。
完全无法调试的东西(如前述的chunks)怎么可能被任何人理解?
祝你好运。
我们选择Next.js,恰恰是因为它在众多企业级产品中作为扩展SDK的唯一选项。
没人愿意接手项目后,向客户推销“用DIY框架替代他们付费购买的产品SDK”这种方案。
那你们并非主动选择,而是被迫接受。
绝大多数情况下,React生态中本就有大量非DIY替代方案——这还是在未探讨“我们是否需要React”的前提下,而答案显然是肯定的。
若由我决定,项目很可能完全不使用JavaScript,或仅采用原生JavaScript。
这很合理,但听起来像是你项目的特定需求。
而这种需求在断言中完全被忽略了:
> 任何情况下都不应再选择Next框架。
大约8年前,当Angular与React的较量还值得关注时,我认为框架已进入最终形态。它们提供基础工具,你就能构建应用程序。我感觉框架创建者不再把我们当成需要手把手指导的婴儿。不知是否因新一代年轻开发者接手,事物开始变得浮华。博客文章不再探讨性能、易用性或通用解决方案,某些标题甚至令人费解。如今根本无暇追踪这些动态。为何路由器需要不断重建和调整?难道我们早就在多年前就确定了路由器的运作方式吗?我们究竟在追求什么创新?难道开发者只是把框架当作周末的实验项目?
> 路由器为何需要持续重建和折腾?难道我们早就在多年前就掌握了路由器的运作原理吗?
不。过去五年最大的创新在于能协调数据加载的路由器——它们能精准预判用户即将访问的内容。
这是个我们苦苦钻研多年的难题。编写优化SQL查询或API请求序列以最少查找获取全部数据, 感觉 就像重复枯燥的公式化工作。于是我们试图用某种智能编译器实现自动化。数据库内的查询规划器、ORM框架、GraphQL协议、路由器、内存管理编译器——它们都在追逐同一个梦想:倘若计算机能“天生”知道如何以最快速度获取精准数据量该多好。
我至少重读了五遍这条评论,每次阅读都感觉要中风。最近每当接触React那些炒作驱动的领域时,类似情况也频频发生。
我本想提出更有建设性的批评,也绝非刻意刻薄(或粗鲁),但你把太多概念混为一谈,导致 这句话根本无法被合理解读 。至少,除非我给你一个绝佳机会让你反驳“不不不你完全曲解了我的意思”,然后再抛出另一个同样令人晕头转向的论点。
你究竟是如何把查询规划器扯进路由器再扯进编译器的?这些东西如何在“追逐同一个梦想”或“可互换”的框架下运作,甚至它们之间是否存在关联,我都完全搞不懂。
–
技术领域竟是少数几个执意自我设限的行业,这既可悲又可怖。走进麦当劳询问流程改进方案时,我敢保证所有建议都直指减轻员工负担——这些建议至少表面上符合企业关心的服务效率目标。
但在科技领域,你一问,对方就会开启诗意之旅,大谈纸杯、得来速点餐器和咖啡机如何共追同一梦想,还坚持要购置十万美元的意式咖啡机——需要耗费十分钟高强度操作才能冲出一杯,这样我就能在下一份薪资高于麦当劳的工作中,获得操作十万美元咖啡机的资格。
我们以前根本不懂如何煮咖啡,那都是错误的,必须把流程复杂化至少十倍。
> 你们究竟如何把查询规划器拖进路由器再塞进编译器?这些东西怎么可能追逐同一个梦想?怎么可能互换?甚至连基本关联都谈不上,我完全搞不懂。
容我展开说明。
在组件化UI中,每个组件负责获取自身数据。你可以声明它们的数据依赖关系。这使组件独立且易于迁移。
你可以随意将某个渲染用户名和头像的组件移到应用的任何位置。它会自动识别当前用户,获取数据并渲染所需内容。这极大提升了复用性和灵活性。
那么当页面上存在多个此类组件时会发生什么?会出现大量无序的数据获取操作。每个组件独立运行,彼此之间缺乏认知,因此无法协调。
但究竟谁掌握着这些组件的信息?没错,路由器(router)知道。因此路由器能够进行更协调的数据获取,为所有组件获取所需数据。
这与经典MVC模式中的控制器职责完全相同——控制器负责编写数据获取逻辑(SQL查询),确保模板(Jinja或其他)调用时数据已准备就绪。本质问题相同,只是命名规范不同。
与ORM类似,这里同样存在N+1问题:当模板访问未预先加载的属性时,会触发大量数据获取(SQL查询)。最终你不得不做大量繁琐工作,将连接操作全部移至控制器,以此欺骗ORM实现数据预加载。
如果你的ORM能理解所有渲染逻辑,并自动生成单条查询获取全部数据,那该多好?确实如此。就像如果你的路由器能预判所有UI组件的获取需求,在初始阶段就发起一次大型请求,那该多好?当然。
现在让我们看看这与查询规划器的关联:如果只需声明所需数据,计算机就能自动从磁盘读取恰当数据量,关联相关部分并输出结果,岂不美哉?无需手动编写大量磁盘读取逻辑,无需循环处理数据清理关联,还能自动最小化磁盘访问次数?当然美哉。
为何非得由路由器承担?更合理的方案是让父级组件协调子组件的数据获取。你提出的方案已远远超出了路由器的常规职责范围。
路由器本质上只是最顶层的组件。它接收输入(如URL),通过巨型开关语句决定渲染内容。
用这种论调,任何东西都能被塞进路由器。
> 那么页面上存在多个此类组件时会怎样?你将面临大量无序的数据获取请求。每个组件独立运行,彼此隔绝,无法协调。
不。你需要编写数据层来消除重复请求。考虑到用户体验通常需要状态管理来处理数据获取,何不将所有逻辑整合到一个整洁的钩子中?
https://tanstack.com/query/latest
我认为症结在于请求层的过度简化,这种“强制”导致路由层出现严重过度设计。
–
顺便说一句,你那些自问自答的反问句在相关性上实在离谱,我懒得深究,只提醒一句:糟糕的类比比没有类比更糟糕。
你试图解释请求瀑布图,直接说请求瀑布图不就行了。必要时查查术语,基本常见问题很少需要你重新定义。
> 你试图解释请求瀑布模型
那么概念上请求瀑布模型与N+1查询问题有何区别?
实际操作中,提及“数据库内的查询规划器”而非请求瀑布模型有何益处?
通过类比说明:协调数据获取很困难,我们在不同场景下已为此努力多年。
类比能帮助我理解事物,并借鉴他人的经验教训。
> 若页面存在多个此类组件会怎样?将产生大量无序的数据获取请求。每个组件独立运行,彼此互不相知,自然无法协调。
tanstack-query等工具解决了这个问题。十个组件共享同一请求时,仅触发一次API调用,响应结果将被共享。
这恰恰说明独立组件的概念本身可能是糟糕的设计。你们试图通过增加复杂性来解决问题,好像乐高积木那样构建应用。我们原本拥有数据层或父级组件来处理协调工作。应用照样能构建且运行良好,浏览器也不必因性能问题而苦苦挣扎。
更何况在此之前,我始终能轻松编写你提到的组件——应该说毫不费力,完全无需应对这种N+1请求的疯狂。那么组件为何需要在大型应用中变成独立应用?
很快你就会告诉我们:这些组件需要独立的基础设施,从不同CDN拉取资源,使用各自的认证和安全上下文,还必须被浏览器沙盒隔离以防止访问彼此的cookie。而这一切都是必要且重要的改进。
Next、InertiaJS和Phoenix实时视图等新技术的承诺在于:无需构建API。其核心理念是:既然前端已知用户所需内容,何必耗费大量时间定义接口来协调数据获取与反序列化?
它们采用不同实现方式,但核心理念一致:缩小前端与后端鸿沟,简化状态管理,同时保持良好性能。
你将截然不同的概念混为一谈。
后端服务于前端是一回事,而将前端状态提升至服务器的框架(如LiveView、Blazor等)则是另一回事。
你描述的这些优势与RSC(响应式服务器端渲染)或原帖作者苦恼的问题毫无关联。React Router提供的加载器就在前端运行,其操作逻辑基本相同。
tRPC/oRPC 不仅省去了构建 API 的大部分繁文缛节,同时避免了为前端隐含创建松散类型 API 的弊端。
不过话说回来,这场讨论其实没什么意义。光看你评论的语气,我就知道你深信不疑,而我并不认同。真正投入的人忙于建设,持怀疑态度者只会寻找抱怨的借口。对吧?
没错,它们的具体实现千差万别,但核心动机一致:手动定义API来维护复杂的前端状态与后端状态并实现同步,本身就是种过度复杂化。再加上如何解决服务器端渲染(SSR)的问题——既要最大限度降低复杂性,又要保持前端响应性。
实时视图方案主张“全部在服务器端渲染,然后通过开放连接持续同步”。这样既保证了响应性,又保留了SSR特性,还省去了精心设计API的麻烦。Next主张“在服务器端渲染,或在客户端渲染,由我们自动判断并映射数据”。Inertia则表示“类似Next思路,但需稍作定义。同样支持在服务器或客户端渲染React或Vue”。
顺带一提,我厌恶Next的方案,认为其思路本末倒置。我认为Inertia更合理,而实时视图才是该理念的“终极形态”。
Next真正擅长的领域是这些延迟优化但高度动态且可并行化的站点:用户端状态极简且贴近用户,采用垂直集成架构,并为前端开发者提供工具。
React向服务器端扩张后虽显凌乱,但复杂度尚可控。
当前状态如同SpaceX火箭发动机仍挂载所有诊断设备,但经过几轮迭代后,其外观与体验都将更显精炼。
…
餐厅的端到端体验确实至关重要——无论是拥有三千颗高特米罗星级的高级餐厅,还是那些内置扭曲激励机制的全球连锁品牌(其菜品本就该限制18岁以上食用,可不仅因为那些超刺激的酱汁)。…
关键在于,当用户规模庞大时,这些细节尤为重要。
…
话虽如此,NextJS文档负责人该下放去全栈矿场干活了。
Next在追逐RSC巨龙之前占据的细分领域,是处理复杂前端状态的单页应用扩展。
React Query和基于atoms的状态管理为Redux/Context地狱提供了优雅替代方案,Next本可暂停重大架构重构,专注于提升用户体验的更新。
他们想垄断无头Shopify/CMS领域,但没必要把Next.js拖下水。这是商业/品牌决策,而非真正惠及现有用户群的举措。
某种程度上确实如此。当影响者文化盛行,初创公司以GitHub表现作为招聘依据时,人们总要设法脱颖而出——比如开发框架。
你抓住了关键点。行业重心已从“解决问题”转向“创造新模式”。激励机制完全扭曲——框架作者从创新表演中获得认可,而非枯燥的可靠性。
我认为部分原因在于Web开发者社区的爆炸式增长。开发者增多=更多人试图留下印记=更高流动率。人人都想成为“修复React”或“重构路由机制”的英雄。
但当你真正构建用户依赖的产品时,就会意识到这种模式的代价。每次破坏兼容性的框架“升级”,都意味着无法投入到功能开发、用户反馈或实际问题解决上。
讽刺的是,最优秀的产品往往基于“枯燥”却稳定可靠的技术。Instagram多年以Django支撑海量用户,Basecamp至今仍用Rails。这些团队专注用户体验,而非追逐最热门的技术栈。
你发现哪些框架/工具能经年累月保持稳定运行?
针对明显由大型语言模型生成的评论,该如何制定审核政策/规范?指出这些评论似乎只会徒增争论,但放任不管又像是让互联网走向死亡的又一步。
我认为应根据具体情况处理。归根结底,人类能写出像AI的文字,AI也能写出像人类的文字,但好答案就是好答案,差答案就是差答案。现有的声望机制和审核规则已具备处理不当回答的多种途径。
我认为互动的价值才是关键。那些让大型语言模型回复的人,究竟是想学习?思考?辩论?还是其他目的?而这种互动对回复者或阅读者而言是否具有价值?对着虚空呐喊却听到百万人的连贯回声,这与真正的对话截然不同。
对于我们管理的几个纯客户端应用,我尚未遇到手动执行 Vite create 并安装 react router 的极限情况。它拥有合理的默认构建配置,在 JavaScript 可能实现的任何“正常工作”定义下,都能“直接运行”。若基础配置变得过于复杂,通常意味着我们过度复杂化了某些环节。
在需要服务器端支持时,Node.js始终让我们感到违和,因此我们根据实际需求坚持使用Java Spring Boot或Flask/FastAPI。
自从React Router与Remix合并升级为React Router v7后,我一直在寻找更简洁的替代方案,最终选择了Wouter,效果尚可。
很想了解你们切换的动机。据我所知,React Router的所有新增功能都是可选的。该库提供三种“模式”[0],其中声明式模式几乎完全复刻了经典库的特性,只是额外提供了可选组件/功能。
不过我确实享受“框架模式”带来的代码拆分功能,以及对SPA/SSR/SSG等策略的支持
[0] https://reactrouter.com/start/modes
我正在框架模式下使用React Router v7,通过服务器端组件直接从数据库获取数据。目前只要避免将服务器代码与客户端组件混用,整体运行尚可。但尽管这种模式能同时编写前端和后端代码,其带来的认知负担却让我越来越觉得得不偿失。
没错,我用的是声明式模式。试过数据模式加加载器,但这种模式始终不合我胃口。
过去三四年间,我在多个中型项目中持续使用Wouter。只要能避免,我绝不会再用react-router:那简直是API频繁变更和自我推销的地狱。
糟糕,Wouter似乎和react-router v6+存在相同问题——嵌套路由无法获取完整参数/路径。
而且缺少react-router v5的路由字符串TypeScript解析功能。https://github.com/molefrog/wouter?tab=readme-ov-file#route-…
> 同样缺少React Router v5的路由字符串TypeScript解析功能。
它确实支持,前提是你指的是自动解析“/foo/:id”并生成类型化“{ id: string? }”路由参数对象的功能?Wouter在使用TypeScript时实现了该功能。
感谢指正,查看类型后我猜是这部分:https://github.com/molefrog/wouter/blob/v3/packages/wouter/t…
没错!就是这个类型。记得以前实现要复杂得多,现在作者似乎直接从“regexparam”包导入类型来处理了。
自从多年前被React Router的迁移问题坑过之后,我所有项目都用wouter至今。
TanStack才是理智的选择,无论是他们的路由器还是start产品。
wouter很棒,直到你需要哈希路由时它就变得一团糟。
我基本认同这个观点,但——如今我多次希望在应用中加入静态网站组件,而在标准Express+React架构下这变得相当棘手。
我认为一定有更好的解决方案。Next.js并非理想选择,Astro也未能完全说服我。令人惊讶的是,这个领域仍有创新空间。
这完全是我们的亲身经历。我们用简单路由器替换Next.js后,各方面都变得更简洁、更快速。替换这个复杂框架的过程堪称一次深刻的学习。
没错,事实证明RSC完全没必要
理论上是个好主意,只是性能需要优化。或许用bun能实现。
可惜Bun在未来数年内都不适合用于任何严肃应用的生产环境,存在太多安全隐患。
真的吗?有相关分析的链接吗?
考虑到Bun团队在API兼容性、工程实力和细节把控方面展现出的成熟度,我对此感到震惊。我所见的一切都表明他们在安全方面绝不会马虎。
问题列表里充斥着段错误的漏洞。至少上次我查看时是这样。但这正是C/C++/Zig等语言的通病。要建立完善的模糊测试流程消除所有漏洞需要大量时间。以Chrome为例,仅需演示内存问题(无需实际漏洞利用)就能获得2万美元赏金。
“性能再提升一步就完美了兄弟,V8很棒但再进一步,我们就能用JS开发CRUD应用了,我保证”
或者你可以使用 React Query/Tanstack Query,避免在 RSC 上浪费周期和带宽,获得用户体验更佳的应用程序(http://ilovessr.com),以及更简单易维护的思维模型。
没错,Vite+React+Tanstack构建的SPA应用确实是多数网页应用的最佳选择。不过对于电商或需要点击谷歌链接后即时加载的页面,我仍会坚持使用nextjs。
从一开始就很明确这并非必要。有趣的是,多少初级开发者会高呼“必须不惜一切代价避免向客户端推送冗余代码否则必死”。事实上,我十多年来构建的React应用从未使用过RSC这类东西,这些应用创造了数百万美元收益,所以根本不成问题。
我们向客户交付数兆字节的安装包,并预加载海量数据。
没人抱怨过。事实上,用户对速度赞不绝口。他们根本不在乎首次加载慢。说不定任务间隙他们早就退出页面了。但一旦进入系统,他们就要求极致流畅。
这完全取决于具体场景。若在博客平台这么做,结果就是像blogspot.com那样永远转圈圈。
同感。我们庞大的SPA应用速度极快——采用服务器端渲染而非流式传输,拥有出色的CWV评分,甚至——天啊——还用了CSS-in-JS!所有这些性能优化都是狗屁谎言。他们用FUD手段摧毁了DX抽象层的美妙设计,我永远无法释怀。
对,忘了提CSS-in-JS!但如今这玩意儿似乎会拖垮网站爬行速度,让应用彻底瘫痪…真有意思
感觉做了这么多工作,最终却又回到Django风格的网站应用模式。这些框架不断重新解决那些早已在JavaScript之外被解决的问题,然后人们还写博客大谈特谈这种“惊喜”。看着看着就觉得有点诡异。
推广包简直是致命毒品
过去二十年我主要扎根于Java和.NET领域,虽然最近三年不得不忍受Next.js的折磨。
如今常有强烈的既视感,仿佛回到了二十年前ASP.NET的Web Forms时代。
工作项目选了Next.js,只因团队懂React,公司其他人也懂React。这正迅速成为我职业生涯中最糟糕的架构决策。真希望当初选了别的框架…
更换路由层的噩梦。这问题为何至今未解?
这正是使用Rails这类稳定框架的被低估优势——本质上与二十年前相同,却拥有大量附加功能。至少我记得路由结构从未发生过重大变更,其他基础构建模块亦是如此。
即使中断十年,重返Rails也能迅速接续先前的工作
为何要在用户资料表单上费力实现服务器端渲染?
SEO无需此操作,缓存毫无意义,服务器渲染速度不会快于客户端,根本无法提升性能。
何必刻意增加复杂性?至少部分痛苦是自找的,或许作者只是选了个糟糕的示例。
症结在于大家都在优化博客点击率而非解决实际问题。“看这个新模式!”能博眼球,“我们保持简单纯粹”却无人问津。微服务浪潮亦是如此——众人因概念新潮蜂拥而至,却在分布式系统地狱里苦苦挣扎多年。
>这是因为Next.js加载服务器组件时,无论如何都会重新挂载整个页面。我曾苦苦哀求Next.js只更新现有DOM并保留状态,但它就是不听。
说得太对了!说得太对了!此刻我简直被点透了!这种行为让我难以置信地抓狂。实在想不通他们为何在未修复此问题的情况下就发布了RSC。
咦,能具体说明下吗?我似乎没遇到过这种情况,可能是我漏掉了什么。
哎哟…正考虑从页面路由切换到这个方案。但这简直背离初衷啊
我注意到作者提到他们当前使用的技术栈是NextJS + Hono,根据博客中的代码,Hono被用作API后端,通过调用
fetchUserInfo等方法为NextJS提供数据。这点让我非常困惑:为什么采用这样的设计?NextJS本身就是全栈框架,更不用说使用RSC时,你完全可以在NextJS中直接从数据库获取用户数据。若选择Hono作为HTTP API(这无可厚非,毕竟它更轻量灵活),那么为何要采用RSC而非直接构建SPA前端?即便使用NextJS编写SPA,将NextJS与Hono结合使用也显得更为合理。我不得不完全剥离路由和延迟加载功能,因为它们运行缓慢且体验糟糕。NextJS确实适合需要次日上线的半静态网站,但一旦涉及复杂场景就会暴露出诸多问题。
NextJS竟成为新开发者的默认工具链,实在荒谬。
令人遗憾的是,那些靠营销呐喊让人们克隆框架的人往往发展最快。
多数框架存在大量冗余抽象层,既可能过度复杂化也可能过度简化产品。
盲目推崇任何强意见解决方案绝非我们该教导开发者的方向…/讽刺
应用路由器简直是过度设计的混乱。“这些规则对普通开发者而言过于晦涩。”
直接用页面路由器吧,它仍在维护且运行良好,静态生成功能对SEO也完全适用。
我使用页面路由器。这种方式对我而言合乎逻辑,而单页应用从未做到过。
最疯狂的是,Pages路由器在我看来本质上就是Perl经典库HTML::Mason的React风味版本。
它提供用于服务器端生成静态HTML的嵌入式代码,基于仓库文件目录结构实现应用路由,并在目录结构中自动分布代码封装器。
https://metacpan.org/pod/HTML::Mason
我曾多年深度使用HTML::Mason,它在大规模应用中的扩展性令人惊叹。
后来Mason取代了HTML::Mason,但我因不再使用Perl而从未接触过它。
https://metacpan.org/pod/Mason
若你喜欢Pages路由器,不妨翻阅Mason用户的古老文献,从中发掘技术诀窍与失传知识。
需知:我虽未用Next开发或维护过大型应用,但曾稍作尝试,并为Next应用中的SDK提供过支持。
Tanstack缺乏后退按钮处理功能令人抓狂。丢弃用户数据的做法完全不可接受。
Wouter在这方面表现更好吗?
能具体说明吗?后退按钮处理存在什么问题?我目前尚未使用过Tanstack
我也很好奇。我正在使用Tanstack Router,尚未发现问题——而我是那种会立即放弃破坏后退按钮等基本浏览器功能的网站的用户
最让人懊恼的时刻是将Levels.fyi从gulp.js迁移到next.js时。页面速度、托管成本等指标全面崩盘。我们正遭遇帖子描述的相同问题,也在权衡迁移方案。请务必避开next.js/vercel。
既然这么糟糕,为什么不直接放弃?
现在更倾向部署到哪里?
路过的各位也请分享意见
我们目前部署在AWS,这点不会变。只需更换框架即可 🙂
最终你们怎么做?还在用nextjs吗?顺便说一句,我是levels.fyi的忠实粉丝。感谢你的工作
感谢!我们仍在使用Next.js。优化/迁移过程中会整理博客分享。幸亏AI让这类大规模重复性迁移变得简单多了。
据我所知,Tanstack甚至不支持RSC。
那又怎样?
我会尝试结合评论审视这篇文章,让讨论更具建设性而非单纯贬低Next.js(作为多年Next.js开发者,我对它相当满意——但确实认同它需要更深层理解)
> React现在用“服务器”和“客户端”指代特定概念,无视了它们原有的定义。这本无不可,但问题在于客户端组件同样能在后端运行
最初为这些组件命名时经历了艰难的讨论。即便采用文章中提议的“后端”和“前端”称谓,也未能清晰阐明它们的行为语义。我理解命名带来的困扰,但这远非“啊我们当初该这么命名”就能解决的复杂问题。
> …这导致服务器端组件变得异常精简,仅负责数据获取,而客户端组件则包含基本静态化的页面版本。
> // HydrationBoundary 是将 JSON 数据从 React 服务器传递至客户端组件的组件。
他们似乎在将Next的原生数据注入机制与TenStacks(另一框架)结合,以便更轻松地在浏览器端获取数据?
以他们WebSocket示例为例:当WebSocket连接发送数据时需要更新用户卡片状态。我不明白直接在客户端组件中使用WebSocket库会有什么问题。我认为这在其他框架中也是必要操作,因此不明白Next.js在此引发了什么问题。
他们的做法简直像在搞黑客手段,很可能正是本节问题的根源。
> 登录状态会影响主页显示,这令人抓狂——客户端明明具备即时呈现页面的全部条件
我不太理解这部分。他们提到应用并非静态而是完全动态的,那么如何避免在页面切换时显示加载状态?
> 无法通过应用路由器实现的加载状态示例:当用户点击Git项目问题页面的用户名跳转至个人主页时。使用loading.tsx时,整个页面仅为骨架结构。但通过TanStack Query建模查询时,可在用户资料和仓库数据加载期间即时显示用户名和头像。服务器端组件无法支持此类导航,因为数据仅存在于渲染组件中,必须重新加载。
可借助第三方库实现跨页面信息复用。例如 motion 的 AnimatePresence 可实现两个 React 状态间的平滑过渡。另一种复用前页数据的方案是直接集成 Next.js 新推出的视图过渡 API:https://view-transition-example.vercel.app/blog
<-注意点击帖子时标题会立即显示> 在工作中,我们仅让 loading.tsx 文件包含 useQuery 调用并展示骨架。这是因为当 Next.js 加载实际服务器组件时,无论如何都会重新挂载整个页面。此处不进行虚拟 DOM 差异比较,意味着所有钩子(useState)在请求完成后都会略微重置。我曾尝试复现一个简单场景,迫使 Next.js 仅更新现有 DOM 并保留状态,但始终无法实现。所幸空白 RSC 调用的耗时足够短。
这似乎是首个问题的衍生现象:试图将两个本不该协同工作的不同水化系统强行结合?
> 独立加载布局是个巧妙构想,但最终显得荒谬——因为这意味着每次布局加载都需重新获取数据。无法共享QueryClient,只能依赖其猴子补丁化的fetch实现来缓存相同GET请求(正如他们承诺的那样)。
或许作者忽略了 React 缓存机制(https://react.dev/reference/react/cache)及其在 next.js 中的应用——通过实现按树形渲染缓存 fetches,完全可规避此问题
> 此方案使初始HTML负载翻倍。实际更糟的是,RSC负载包含JS字符串字面量中的JSON数据,其格式远不如HTML高效。虽然brotli压缩效果良好且浏览器渲染迅速,但这种设计仍属资源浪费。采用数据注入模式时,本地数据至少可复用于交互操作及其他页面。
是的,数据双重传输是实现数据活化的必要架构障碍。此前已通过AnimatePresence等方案探讨过跨页面复用数据的可行性。
关键在于RSC有效负载位于HTML末尾。由于HTML默认采用流式传输,这不会影响首次渲染时间。其他框架同样需要实现类似机制(方式不同但本质相同)
我完全理解作者的挫败感。Next.js 并非完美,我也对其诸多设计存有异议——尤其厌恶其拦截/并行机制,配置 ISR/PPR 更是噩梦般的体验。只是觉得有必要回应部分评论,或许能对他们有所帮助?
首先我会移除坦斯塔克,因为它与Next.js架构相冲突。
或者干脆整体迁移到别处吧 🙂
> 但我确实认同这需要更深入的理解
你认同谁的观点?
“对基础设计决策存在重大分歧”并非理解不足。问题出在NextJS本身。
…或者干脆不用服务器端组件 🙂
没想到这么多人喝了RSC迷魂汤。我试用不到一小时就痛苦地意识到:原本简单的事变得多么复杂。
我也完全不理解它的用武之地。
要么你是在构建SEO优化的网站,追求初始页面加载速度极致。这种情况直接做静态网站就行,用任何技术编译成HTML+CSS即可。
要么你是在开发“应用”,这种情况下用户会停留较长时间,庞大的初始数据包最终会被缓存,根本无需每次点击都重新发送。因此请全力投入客户端渲染,适度简化技术栈。代码拆分、预加载等优化手段依然可行,但无需维持这种怪异的混合模式——某些功能在某处有效却在别处失效。
这基本就是作者的观点,很高兴看到人们开始意识到这一点。
> 或者你正在构建一个“应用”[…] 那就彻底采用客户端渲染吧
我更希望企业能更进一步,直接构建PWA。这能让你调用大量Web API,进一步简化技术栈。
我认同许多企业为无需SEO优化的Web应用选择Nextjs确实令人费解,但某些复杂的渲染策略对Web应用仍具价值——即便是PWA也不例外。
若要构建SEO优化网站,根本无需静态网站。直接采用常规SSR(甚至无需流式处理),加上CDN-Cache-Control头部即可。响应时间将缩短至数十毫秒。
你会发现代码逻辑清晰度提升十倍。
…哦等等,这不正是作者最终的做法嘛哈哈
你试过Nuxt.js吗?
Nuxt确实很棒,但最近被Vercel收购了,我们都在预料它会逐渐变质,最终沦为Next.js的翻版。
用Homeroll Vite加Express搞定就行。
我用过应用路由器,理解其原理后确实不错。但它堆砌了大量没人需要的复杂性。普通React单页应用用Vite+Wouter+React-Query就足够出色了。