Next.js 的工程化之痛:App Router 真的好用吗
写前端这些年,接触过的框架不少,但像 Next.js 这样让人感到如此拧巴的确实不多。客观地说,App Router 或许是近年来争议最大的架构升级,而令人无奈的是,这种反直觉的开发范式正逐渐成为 Next.js 官方推崇的标准。
被割裂的开发体验
React 的魅力很大程度上源于 Hooks 带来的逻辑复用能力。然而在 App Router 体系下,服务端组件(Server Component)与 Hooks 彻底绝缘。
更麻烦的是 Client Component 的向下传染性:一旦父级组件声明为客户端组件,其下的所有子组件在逻辑上都会被划入客户端边界。这种级联效应导致我们在架构设计时束手束脚。即便是像 Redux 这样成熟的状态管理方案,在 App Router 下也显得格格不入,不得不采用一种迂回且略显臃肿的写法:
"use client"; import { Provider } from "react-redux"; import { useRef } from "react"; import { makeStore, AppStore } from "./store"; export default function Providers({ children }: { children: React.ReactNode }) { const storeRef = useRef<AppStore | null>(null); if (!storeRef.current) { storeRef.current = makeStore(); } return <Provider store={storeRef.current}>{children}</Provider>; }
命名的误导与认知的陷阱
App Router 的另一个槽点在于其命名逻辑。初学者极易产生误解,认为只有 Server Component 才具备 SSR 能力,而一旦标注
use client,组件就会退化为传统的 SPA 模式。事实上,Client Component 依然会在服务端预渲染,真正的区别在于数据获取的阶段——写在
useEffect 里的请求无法参与 SSR。这种术语上的模糊性,无形中拉高了团队的沟通成本和理解门槛。顺便一提,两者真实的渲染逻辑如下:
- Server Component (RSC): 在服务器端运行并渲染,生成的静态 HTML 直接发给浏览器。它从不下载到客户端,也不参与注水。
- Client Component (CC): 同样在服务器预渲染(Prerendering)生成初始 HTML,确保首屏可见;到达浏览器后再进行注水(Hydration),绑定事件监听器。
既然都能 SSR,为何还要强行区分?
这种增加心智负担的区分,本质上是为了解决 Web 开发中两个极端矛盾的痛点:包体积与数据安全。
零包体积(Zero Bundle Size)
这是区分两者的最大价值。Server Component 的代码永远不会发送到浏览器。如果在服务器组件里用了一个巨大的库(比如处理 Markdown 的几百 KB 的插件),用户下载的 JS Bundle 体积依然是 0。而 Client Component 为了在浏览器端实现交互,它引用的所有库都必须随着 JS Bundle 下载到本地。通过这种强制区分,Next.js 迫使我们保持交互部分足够轻量,尽可能把重型渲染逻辑留在服务器。
我只能说,我不认同这种强制的区分方式,这给开发者带来了太多的心智负担,但是我尊重 Nextjs 团队的选择。
数据安全
在 Server Component 里,可以直接写 SQL 查询,甚至直接读取服务器上的密钥文件,因为这些代码物理上无法被浏览器访问。而在 Client Component 里,只能通过 API 接口获取数据。
动态路由缓存失效?
在 App Router 中,另一个让人崩溃的点是:动态路由的缓存会莫名其妙的失效。为了让一个动态路由能像以前一样被 CDN 或服务器稳稳缓存住,开发者需要写出这种冗余的配置:
export const dynamicParams = true; // 1. 允许访问构建时不存在的路径,默认值为true export const dynamic = "force-static"; // 2. 强行关闭所有动态灵活性 export const revalidate = 3600; // 3. 开启 ISR,每小时续命一次缓存 export async function generateStaticParams() { return []; } // 4. 按需生成 URI
对于动态路由,不配置就没有缓存,开发者不是在写逻辑,而是在各种冗余的配置项中,小心翼翼的平衡着框架那捉摸不定的渲染偏好。
如何在约束下优雅选择
在这种强硬且无理的范式下,开发者思考方式要改变:
- 默认使用 Server Component:为了极致的加载速度,为了天然的 SEO 优势。
- 精准抽离 Client Component: 仅当组件需要
onClick、useState、useEffect或者类似usePathname()这种浏览器感知能力时,才将该节点标记为"use client"。
框架还是枷锁
如今的 Next.js 越来越像一个庞大且沉重的操作系统。它不再仅仅是一个 UI 框架,而是强行定义了一整套复杂的规则。开发者被迫在各种约束下编写代码,不仅要考虑业务逻辑,还要不断地在服务端与客户端的边界线上反复权衡。他已经不再是以前那个好用的 Nextjs 了。