去年年底,我接了一个海外客户的项目,要求使用 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>
  )
}

性能优化的关键点

经过这个项目的实践,我总结了几个关键的性能优化点:

  1. 合理使用 Server Components

    不是所有组件都适合作为 Server Components。需要频繁更新的数据展示组件,反而更适合作为 Client Components,避免频繁的服务器端渲染。

  2. 优化数据加载策略

    使用 Suspense 和 React.lazy() 实现更细粒度的加载控制,配合 loading.tsx 提供更好的加载体验。

  3. 缓存策略的调整

    根据数据的实时性要求,合理设置 revalidate 时间,避免不必要的重新验证。

写在最后

Next.js 13 的 App Router 确实带来了很多令人兴奋的新特性,但也需要我们改变一些既有的开发习惯。通过这个项目,我深刻体会到了它的强大之处,也踩了不少坑。希望这些经验能帮助到同样在使用 App Router 的同学。

如果你也在使用 Next.js 13,欢迎在评论区分享你的经验和想法。如果觉得这篇文章有帮助,别忘了点个赞 👍