Next.js 15 和 16 的发布,并没有带来翻天覆地的 API 变更,反而在 App Router 稳定运行两年后,做了一轮非常务实的修补。很多团队已经从“要不要用 App Router”的纠结,转向了“怎么在复杂项目里不让它变得一团糟”。这篇文章不打算逐条翻译 Changelog,而是结合工程里实际踩过的坑,聊聊在 App Router 时代,哪些做法真的能帮项目稳住复杂度。

App Router 带来的最核心变化,不是文件夹路由,而是 React Server Components(RSC)的默认引入。这意味着你的组件树里,有些代码只在服务端运行,有些在客户端运行,而边界的选择会直接影响性能、包体积和开发体验。很多团队遇到的问题,根源都在于对这条边界不够敏感。
路由组织:从“能跑就行”到“可维护”
App Router 的文件约定比 Pages Router 更严格,layout、page、loading、error 这些特殊文件很容易让目录显得杂乱。一个常见的误区是,把所有页面都堆在 app 目录下,没有分层。随着路由数量增长,共享 layout 和局部状态管理会变得很难追踪。
比较好的做法是,尽早把路由进行分组(Route Groups),用 (marketing)、(dashboard) 这样的文件夹把不同业务域隔开。分组不会影响 URL 路径,但可以让 layout 的组织更清晰。同时,避免创建太深的嵌套 layout,三层以上的 layout 往往意味着需要重新审视页面结构是否合理。
另一个容易忽视的点是,page.tsx 本身不应该承担太多逻辑。可以把页面拆成“容器组件 + 展示组件”的模式,容器组件负责数据获取,展示组件保持纯客户端交互。这样不仅让组件职责更清晰,也方便后续迁移到 Partial Prerendering 等新特性。
数据获取:别再用老思路写新架构
在 Pages Router 里,我们习惯用 getServerSideProps 或 getStaticProps 来获取数据,然后通过 props 传递给组件。App Router 完全抛弃了这些方法,数据获取直接发生在 Server Components 中,用 async/await 配合 fetch 或 ORM 即可。
很多开发者会无意识地把所有数据获取都放在 Server Components 里,然后通过 props 层层传递,导致链路很长。更好的做法是,在需要数据的位置就近获取,利用 React 的自动请求去重机制,避免重复请求。比如一个页面需要用户信息和文章列表,可以在 UserMenu 组件里直接获取用户数据,在 ArticleList 里获取文章,而不是在顶层 page.tsx 一把抓。
下面这段代码展示了在 Server Component 中直接获取数据并渲染列表的典型模式,配合 loading.tsx 可以获得自然的加载状态:
// app/posts/page.tsx
import { getPosts } from '@/lib/db';
export default async function PostsPage() {
const posts = await getPosts();
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
需要注意的是,fetch 默认的缓存策略在 Next.js 15 中发生了变化,不再强制缓存所有请求。如果你依赖了旧版本的默认缓存行为,升级后可能会发现数据不再按预期更新。因此在生产环境中,务必显式声明 cache: 'force-cache' 或 next: { revalidate: 3600 },避免隐式行为带来的不确定性。
缓存策略:从“粗暴失效”到“精准控制”
Next.js 的缓存层一直很强大,但也很容易让人困惑。在 15/16 版本中,缓存机制被拆分成更细粒度的控制:请求记忆化、数据缓存、全路由缓存和路由器缓存。理解这四层缓存各自的作用域和失效条件,是避免生产事故的关键。
下面这张表对比了不同场景下推荐的缓存策略:
| 场景 | 推荐策略 | 失效方式 | 适用条件 |
|---|---|---|---|
| 静态首页 / 博客文章 | 全路由缓存 + ISR | revalidate 定时触发 | 内容不频繁变化,可接受秒级延迟 |
| 用户仪表盘 | 无缓存或短 TTL | 请求时重新验证 | 数据实时性要求高,但可接受短暂过期 |
| 产品列表页 | 数据缓存 + 客户端请求 | 标签式按需失效 | 需要按业务事件精准刷新 |
| 实时协作应用 | 完全跳过缓存 | 无缓存 | 数据频繁变化,延迟不可接受 |
一个常见的误区是,认为只要设置了 revalidate 就万事大吉。实际上,当页面同时使用了静态生成和动态请求时,缓存行为可能变得非常复杂。建议在开发阶段多用 export const dynamic = 'force-dynamic' 明确标记动态路由,并在 CI 流程中加入对缓存策略的检查,避免上线后才发现数据陈旧。
服务器组件与客户端组件的边界艺术
RSC 的引入让组件边界变得模糊,很多团队在初期会犯两种错误:要么把所有组件都写成 Server Component,导致交互能力缺失;要么无差别地在文件顶部加上 'use client',把客户端包体积撑得很大。
正确的思路是,把客户端组件当作“交互岛屿”,只在需要浏览器 API 或状态管理的地方使用。尽量让客户端组件保持叶子节点,避免将整个子树标记为客户端。如果一组组件中只有一小部分需要交互,可以把那部分提取成独立的客户端组件,其余部分继续留在服务端。
例如,一个文章页面中,正文渲染、SEO 元数据、评论列表的首屏内容都可以在服务端完成,只有“点赞按钮”和“评论输入框”需要用客户端组件实现。这样既保证了首屏性能,又不会牺牲交互体验。
在真实项目中,我们经常会遇到需要将服务端数据传递给客户端组件的情况。这时可以通过 props 将序列化后的数据从 Server Component 传入 Client Component,而不要试图在客户端组件中直接调用服务端数据源。Next.js 会安全地处理这种传递,但你需要确保传递的数据是 JSON 可序列化的,函数或复杂对象会导致问题。
流式渲染与 Partial Prerendering:渐进式落地的思路
Next.js 14 开始引入 Partial Prerendering(PPR),到 15/16 版本已经趋于稳定。它的核心思想是,将页面的静态外壳提前生成,而动态内容以流的形式后续填充。这比传统的全静态或全动态渲染更灵活,但也对页面结构设计提出了更高要求。
很多团队想立刻在整个应用中启用 PPR,但实际效果往往不如预期。原因在于,如果页面结构没有为流式渲染做适配,动态内容的延迟会阻塞整个页面的交互。更好的做法是,先在一个非关键页面上试验,比如后台管理系统的统计面板,逐步优化 Suspense 的边界设置,再推广到核心业务页面。
使用 Suspense 包裹动态组件时,记得提供合理的 fallback。如果 fallback 过于简陋,会带来明显的布局跳动;如果过于复杂,又失去了流式渲染的意义。通常用骨架屏(Skeleton)是一种折中方案,但要确保骨架屏的尺寸与真实内容大致匹配。
下面是几个在 App Router 项目中容易被忽视的实践要点,值得在 Code Review 中反复确认:
- 每个
page.tsx是否都有对应的loading.tsx和error.tsx,避免路由切换时出现白屏或未捕获错误。 - 是否在 Server Component 中引入了仅客户端的依赖(如
useEffect),导致构建失败。 - 是否过度使用
useSearchParams导致整个页面退化为客户端渲染,破坏了 SSR 的优势。 - 国际化路由是否在 Middleware 中统一处理,而不是在每个页面里重复判断。
从 Pages Router 迁移:不是简单的重命名
很多中大型项目仍然运行在 Pages Router 上,迁移并非一蹴而就。Next.js 允许两种路由共存,所以可以渐进式迁移。先从非核心的、独立的路由开始,比如帮助中心、关于页面,逐步积累对 App Router 的熟悉度,再迁移主线业务。
迁移过程中最容易踩坑的是数据获取方式的转换。原来在 getServerSideProps 中串行请求多个接口,在 App Router 中可以直接在组件内并行请求,性能会有明显提升,但需要调整错误处理逻辑。另外,原来依赖 _app.tsx 和 _document.tsx 的全局布局,现在需要改用 Root Layout 和 Metadata API,这涉及到 SEO 相关代码的重写。
一个务实的策略是:新功能全部用 App Router 开发,旧路由保持不动,直到业务价值明确时再迁移。不要为了技术升级而盲目投入资源,评估迁移成本与收益的平衡点,往往比技术本身更重要。
在 Next.js 15/16 的语境下,App Router 已经不是一个需要“尝鲜”的特性,而是构建生产级 React 应用的主流方式。它能带来的不仅是性能提升,更是一种更清晰的职责划分模型。但所有这些好处,都建立在团队对服务器组件、缓存分层和流式渲染有足够理解的基础上。工具本身不会自动产生好架构,真正决定项目质量的,还是写代码的人对边界的把握。
原创文章,作者:fudengji,如若转载,请注明出处:https://fudengji.cn/article/396/