Next.js 14 实战:使用 App Router 构建高性能 React 应用

"你们的网站加载速度太慢了,而且 SEO 效果很差。"上个月,我们接到了一个来自海外客户的紧急需求。他们的电商网站是用传统的 React SPA 构建的,在性能和搜索引擎优化方面都遇到了瓶颈。作为技术负责人,我立刻想到了 Next.js 14 的 App Router。????

今天,我想和大家分享这个项目的重构经历。从技术选型到实际落地,我们是如何一步步优化网站性能的。希望能给同样面临类似挑战的朋友一些启发!

为什么选择 Next.js 14 的 App Router?

说实话,最开始团队里对于是否要用 App Router 是有分歧的。有同事担心它太新,文档不够完善。但经过深入研究,我们发现 App Router 完美解决了我们的痛点:

// app/layout.tsx
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>
        <Header /> {/* 这是一个服务器组件 */}
        {children}
        <Footer /> {/* 这也是一个服务器组件 */}
      </body>
    </html>
  )
}

这种基于文件系统的路由方式不仅直观,更重要的是它默认使用服务器组件(Server Components)。这意味着大部分组件都在服务器端渲染,显著减少了发送到浏览器的 JavaScript 代码量。

性能优化实践

1. 合理使用服务器组件和客户端组件

在重构过程中,我们发现一个关键点:不是所有组件都适合作为服务器组件。这里分享一个我们总结的判断标准:

// app/products/[id]/page.tsx
import { ProductDetails } from './ProductDetails' // 服务器组件
import { AddToCartButton } from './AddToCartButton' // 客户端组件

export default async function ProductPage({ params }: { params: { id: string } }) {
  // 直接在服务器端获取数据
  const product = await getProduct(params.id)
  
  return (
    <div className="product-page">
      {/* 静态内容使用服务器组件 */}
      <ProductDetails product={product} />
      
      {/* 交互部分使用客户端组件 */}
      <AddToCartButton productId={product.id} />
    </div>
  )
}

2. 数据获取策略优化

Next.js 14 提供了强大的数据获取功能。我们充分利用了这一点:

// utils/products.ts
export async function getProducts() {
  // ???? 使用 Next.js 的缓存机制
  const res = await fetch('https://api.mystore.com/products', {
    next: {
      revalidate: 3600 // 每小时重新验证一次
    }
  })
  
  if (!res.ok) {
    // 错误处理很重要!
    throw new Error('Failed to fetch products')
  }
  
  return res.json()
}

3. 图片优化和布局转移

Next.js 的 Image 组件是一个宝藏,它帮我们解决了很多性能问题:

// components/ProductImage.tsx
import Image from 'next/image'

export function ProductImage({ src, alt }: { src: string; alt: string }) {
  return (
    <div className="relative w-full aspect-square">
      <Image
        src={src}
        alt={alt}
        fill
        sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
        priority={false}
        className="object-cover"
      />
    </div>
  )
}

优化后的效果立竿见影:

  • 首次内容渲染(FCP)从 2.8s 降到了 0.8s
  • 最大内容绘制(LCP)从 4.2s 降到了 1.5s
  • Core Web Vitals 全部达到了绿色标准

踩坑经历和解决方案

开发过程中确实遇到了一些挑战,分享几个印象深刻的:

1. 状态管理的取舍

最开始我们想当然地把所有状态都放在了客户端。后来发现这样做反而影响了性能,于是我们采用了一个混合方案:

// app/products/layout.tsx
export default function ProductsLayout({
  children,
  modal
}: {
  children: React.ReactNode
  modal: React.ReactNode
}) {
  return (
    <>
      {/* 主内容区使用服务器组件 */}
      {children}
      
      {/* 模态框使用客户端组件 */}
      {modal}
    </>
  )
}

2. 路由预加载策略

Next.js 的预加载功能很强大,但需要合理使用:

// components/ProductLink.tsx
'use client'

import { useCallback } from 'react'
import { useRouter } from 'next/navigation'

export function ProductLink({ id }: { id: string }) {
  const router = useRouter()
  
  // 使用 useCallback 优化性能
  const handleClick = useCallback(() => {
    // 预加载下一页数据
    router.prefetch(`/products/${id}`)
  }, [id, router])
  
  return (
    <button
      onClick={handleClick}
      onMouseEnter={handleClick} // 鼠标悬停时预加载
    >
      查看详情
    </button>
  )
}

3. 缓存策略的优化

缓存是一个容易被忽视的优化点。我们通过实践总结出了一套规则:

// app/api/products/route.ts
import { NextResponse } from 'next/server'

export async function GET() {
  try {
    const products = await getProducts()
    
    // 设置适当的缓存头
    return NextResponse.json(products, {
      headers: {
        'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=86400'
      }
    })
  } catch (error) {
    console.error('Failed to fetch products:', error)
    return NextResponse.error()
  }
}

项目成果

经过一个月的重构,我们的网站性能得到了显著提升:

  • Google PageSpeed Insights 分数从 65 提升到 95
  • 用户平均访问时长增加了 40%
  • 跳出率下降了 25%
  • 转化率提升了 15%

最让我欣慰的是收到客户的反馈:"网站速度快得惊人,感觉像是本地应用一样流畅!"这让所有的努力都值得了。????

写在最后

Next.js 14 的 App Router 确实是一个革命性的更新,它让我们能够用更现代的方式构建 React 应用。如果你也在考虑是否要升级到 App Router,我的建议是:先从小功能开始尝试,逐步积累经验,最后再考虑大规模重构。

有什么问题欢迎在评论区讨论,我们一起学习进步!

如果觉得有帮助,别忘了点赞关注,我会继续分享更多 Next.js 和 React 的实战经验~

上一篇:如何从 COS 下载文件并保存到指定路径或直接读取内容


下一篇:深入探讨C++中的模板