去年年底,我接了一个海外客户的项目,要求使用 Next.js 13 的 App Router 开发一个数据分析平台。说实话,当时虽然对 Next.js 已经很熟悉了,但对 App Router 这个相对较新的特性还是有些忐忑。现在项目已经成功上线,我想和大家分享一下在这个过程中的实战经验和踩坑记录。
为什么选择 App Router?
最开始和客户沟通技术选型时,我其实在犹豫要不要用 App Router。毕竟 Pages Router 已经用了很多年,相当稳定。但仔细评估后,还是决定采用 App Router,主要考虑了这几个方面:
首先,App Router 采用的 React Server Components 架构能带来更好的性能。在我们的数据分析平台中,有大量的数据展示组件,如果全部在客户端渲染,不仅初始加载慢,而且会占用大量客户端资源。使用 Server Components,我们可以在服务器端完成大部分渲染工作,只将必要的交互部分放在客户端。
其次,App Router 的并行路由和拦截路由特性,完美解决了我们的一些交互需求。比如数据分析平台需要在查看数据列表时,点击某条数据在右侧弹出详情面板,这种场景用拦截路由实现非常优雅。
实战经验分享
1. Server Components 的正确使用姿势
在实际开发中,我发现很多开发者对 Server Components 的使用还存在误解。最常见的问题是不清楚什么时候该用 Server Components,什么时候该用 Client Components。
我总结了一个简单的判断标准:如果组件需要处理用户交互(比如 onClick)、使用浏览器 API(比如 window)或者使用 React hooks,那就必须用 Client Components;其他情况,优先使用 Server Components。
举个例子,在我们的数据图表组件中:
// DataChart.tsx
'use client' // 因为需要用到 echarts,所以标记为 client component
import { useEffect, useRef } from 'react'
import * as echarts from 'echarts'
export default function DataChart({ data }) {
const chartRef = useRef(null)
useEffect(() => {
const chart = echarts.init(chartRef.current)
chart.setOption({
// 图表配置
})
}, [data])
return <div ref={chartRef} style={{ width: '100%', height: '400px' }} />
}
// DataDisplay.tsx
// 这个组件不需要标记 'use client',默认是 Server Component
import { fetchData } from '@/lib/data'
import DataChart from './DataChart'
export default async function DataDisplay() {
// 在服务器端获取数据
const data = await fetchData()
return (
<div>
<h2>数据分析</h2>
<DataChart data={data} />
</div>
)
}
2. 数据获取的优化策略
在 App Router 中,数据获取的方式发生了很大变化。我们不再需要使用 getStaticProps 或 getServerSideProps,而是可以直接在组件中使用 async/await。
但是,这里有个容易被忽视的性能优化点。看下面这个例子:
async function getData(id: string) {
const res = await fetch(`https://api.example.com/data/${id}`, {
next: {
revalidate: 3600 // 缓存一小时
}
})
return res.json()
}
export default async function Page({ params }: { params: { id: string } }) {
const data = await getData(params.id)
return <div>{/* 渲染数据 */}</div>
}
这段代码看起来没什么问题,但在实际项目中,我们发现当用户快速切换不同的数据页面时,性能表现并不理想。后来我们采用了 React Suspense 和并行数据请求的方式进行优化:
import { Suspense } from 'react'
import Loading from './loading'
// 预加载数据
const preloadData = (id: string) => {
void getData(id)
}
export default async function Page({ params }: { params: { id: string } }) {
// 路由变化时预加载下一页数据
return (
<Suspense fallback={<Loading />}>
<DataContent id={params.id} />
</Suspense>
)
}
3. 路由拦截的实践技巧
App Router 的路由拦截(Intercepting Routes)是一个非常强大的特性。在我们的项目中,最典型的应用是实现类似 Modal 弹窗的数据详情页:
app/
data/
page.tsx
[id]/
page.tsx
@modal/
[id]/
page.tsx
这种结构允许我们在列表页面点击某条数据时,以 Modal 形式展示详情,而直接访问详情页面时则显示完整页面。这大大提升了用户体验。
但在实践中我们也发现了一个问题:如果 Modal 中的内容较多,第一次加载时会有明显的延迟。解决方案是使用 Suspense 配合 loading.tsx:
// @modal/[id]/page.tsx
export default async function ModalPage({ params }: { params: { id: string } }) {
return (
<div className="modal">
<Suspense fallback={<LoadingSpinner />}>
<DataDetail id={params.id} />
</Suspense>
</div>
)
}
性能优化的关键点
经过这个项目的实践,我总结了几个关键的性能优化点:
-
合理使用 Server Components
不是所有组件都适合作为 Server Components。需要频繁更新的数据展示组件,反而更适合作为 Client Components,避免频繁的服务器端渲染。
-
优化数据加载策略
使用 Suspense 和 React.lazy() 实现更细粒度的加载控制,配合 loading.tsx 提供更好的加载体验。
-
缓存策略的调整
根据数据的实时性要求,合理设置 revalidate 时间,避免不必要的重新验证。
写在最后
Next.js 13 的 App Router 确实带来了很多令人兴奋的新特性,但也需要我们改变一些既有的开发习惯。通过这个项目,我深刻体会到了它的强大之处,也踩了不少坑。希望这些经验能帮助到同样在使用 App Router 的同学。
如果你也在使用 Next.js 13,欢迎在评论区分享你的经验和想法。如果觉得这篇文章有帮助,别忘了点个赞 👍