返回博客

用 Next.js 构建高性能作品集网站

2025/8/16 分钟阅读

用 Next.js 构建高性能作品集网站

作品集网站是开发者的名片。它不需要复杂的业务逻辑,但对首屏速度、视觉流畅度和 SEO 有着极致要求。Next.js 的 App Router 架构,恰好为这类场景提供了完美的技术基座。

一、为什么选 Next.js

作品集网站通常具有以下特征:

  • 内容相对静态:项目介绍、关于我、联系方式,更新频率低
  • 富媒体密集:大量高清项目截图、视频预览、WebGL 背景
  • SEO 敏感:希望潜在雇主/客户通过搜索引擎找到自己
  • 体验即产品:加载速度、转场动画、交互反馈直接影响第一印象

Next.js 的 App Router 提供了几项关键能力:

特性对作品集的价值
React Server Components (RSC)组件默认在服务端渲染,减少客户端 JS 体积
next/image自动 WebP/AVIF 转换、响应式尺寸、懒加载、防布局偏移
next/font字体自托管,零布局偏移(FOIT/FOUT),无需外部请求
Static Site Generation (SSG)构建时生成纯 HTML,CDN 边缘缓存,毫秒级首屏
Streaming & Suspense渐进式加载,关键内容优先渲染,非关键模块延后

二、项目架构设计

my-portfolio/
├── app/
│   ├── layout.tsx          # 根布局:字体、元数据、全局样式
│   ├── page.tsx            # 首页
│   ├── about/
│   │   └── page.tsx        # /about
│   ├── projects/
│   │   └── page.tsx        # 项目列表
│   ├── projects/[slug]/
│   │   └── page.tsx        # 项目详情(动态路由)
│   └── api/                # 可选:联系表单 API
├── components/
│   ├── ui/                 # 基础组件(Button, Badge)
│   ├── sections/           # 页面级区块(Hero, ProjectsGrid)
│   └── providers.tsx       # Context / Theme Provider
├── lib/
│   └── projects.ts         # 项目数据(MDX / JSON / CMS)
├── public/
│   └── images/             # 静态资源
└── next.config.js

关键决策

  • 数据层:项目数据直接放在 lib/projects.ts 或本地 MDX 文件中,无需后端/CMS。作品集内容不会频繁变更,构建时静态生成是最优解。
  • 样式方案:Tailwind CSS + CSS Variables。Tailwind 的 JIT 引擎只打包用到的样式,对性能极度友好。
  • 动画库:GSAP + @gsap/react。GSAP 的 will-change 管理和 ScrollTrigger 在作品集滚动动画中表现优异,且支持服务端渲染。

三、性能优化实战

3.1 图片:作品集的性能杀手

项目截图往往是 LCP(Largest Contentful Paint)元素。next/image 是优化核心:

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

export function ProjectCard({ project }) {
  return (
    <article className="group relative overflow-hidden rounded-xl">
      <Image
        src={project.coverImage}
        alt={project.title}
        width={800}
        height={600}
        priority={project.featured}      // 首屏图片优先加载
        placeholder="blur"               // 模糊占位,防布局偏移
        blurDataURL={project.blurHash}   // 低质量占位图 Base64
        sizes="(max-width: 768px) 100vw, 50vw"
        className="transition-transform duration-500 group-hover:scale-105"
      />
      <div className="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent" />
      <h3 className="absolute bottom-4 left-4 text-white text-xl font-bold">
        {project.title}
      </h3>
    </article>
  )
}

优化要点

  • priority:首屏可见图片必须加,否则会被懒加载延迟,拖累 LCP
  • placeholder="blur":配合 blurDataURL(可用 plaiceholder 库在构建时生成),消除图片加载时的布局跳动
  • sizes:告诉浏览器不同视口下的实际渲染尺寸,避免下载过大图片
  • 格式:Next.js 自动提供 WebP/AVIF 格式,现代浏览器可节省 30%-50% 体积

3.2 字体:消除布局偏移

使用 next/font 自托管字体,完全消除外部字体请求带来的 FOUT/FOIT:

// app/layout.tsx
import { Inter, Space_Grotesk } from 'next/font/google'
import './globals.css'

const inter = Inter({
  subsets: ['latin'],
  variable: '--font-inter',
  display: 'swap',          // 关键:使用 swap 策略
})

const spaceGrotesk = Space_Grotesk({
  subsets: ['latin'],
  variable: '--font-space',
  display: 'swap',
})

export const metadata = {
  title: 'Alex Chen | Full Stack Developer',
  description: 'Portfolio of Alex Chen - building fast, accessible web experiences.',
}

export default function RootLayout({ children }) {
  return (
    <html lang="zh-CN" className={`${inter.variable} ${spaceGrotesk.variable}`}>
      <body className="font-sans antialiased bg-white text-gray-900">
        {children}
      </body>
    </html>
  )
}
/* globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer base {
  body {
    font-family: var(--font-inter), system-ui, sans-serif;
  }
  h1, h2, h3 {
    font-family: var(--font-space), system-ui, sans-serif;
  }
}

为什么不用 Google Fonts CDN?

  • next/font 在构建时下载字体文件,与 HTML 同域部署
  • 自动注入 font-display: swap,确保文字立即显示(先 fallback 字体,再切换)
  • 零额外的 DNS 查询和 HTTPS 握手

3.3 路由渲染策略:SSG 为主,SSR 兜底

作品集页面几乎全是静态内容,应尽可能使用 SSG:

// app/projects/[slug]/page.tsx
import { notFound } from 'next/navigation'
import { getProjectBySlug, getAllProjects } from '@/lib/projects'

// 1. 生成所有静态路径
export async function generateStaticParams() {
  const projects = getAllProjects()
  return projects.map((p) => ({ slug: p.slug }))
}

// 2. 页面级元数据
export async function generateMetadata({ params }) {
  const project = getProjectBySlug(params.slug)
  if (!project) return { title: 'Not Found' }
  return {
    title: `${project.title} | Alex Chen`,
    description: project.summary,
    openGraph: {
      images: [{ url: project.ogImage }],
    },
  }
}

export default function ProjectPage({ params }) {
  const project = getProjectBySlug(params.slug)
  if (!project) notFound()

  return (
    <main className="max-w-4xl mx-auto px-6 py-20">
      <h1 className="text-4xl font-bold mb-6">{project.title}</h1>
      <div className="prose prose-lg">
        {/* 项目详情内容 */}
      </div>
    </main>
  )
}

关键点

  • generateStaticParams:构建时预渲染所有项目详情页,用户访问时直接返回 CDN 缓存的 HTML
  • generateMetadata:每个页面生成独立的 <title> 和 OG 标签,SEO 友好
  • notFound():数据不存在时返回 404,避免软 404 影响搜索引擎评分

3.4 组件级懒加载与代码分割

非首屏组件(如联系表单、评论区、重型动画)应延迟加载:

// app/page.tsx
import { Suspense } from 'react'
import dynamic from 'next/dynamic'
import { Hero } from '@/components/sections/Hero'
import { FeaturedProjects } from '@/components/sections/FeaturedProjects'

// 懒加载非首屏区块
const ContactSection = dynamic(
  () => import('@/components/sections/Contact').then((mod) => mod.Contact),
  { ssr: false } // 联系表单无需 SEO,纯客户端渲染
)

const Testimonials = dynamic(
  () => import('@/components/sections/Testimonials').then((mod) => mod.Testimonials),
  { loading: () => <p>Loading testimonials...</p> }
)

export default function HomePage() {
  return (
    <>
      <Hero />                    {/* 关键渲染路径,同步加载 */}
      <FeaturedProjects />        {/* 首屏内容,同步加载 */}
      
      <Suspense fallback={<div className="h-96" />}>
        <Testimonials />           {/* 滚动后才可能看到,懒加载 */}
      </Suspense>
      
      <ContactSection />           {/* 页面底部,懒加载 */}
    </>
  )
}

策略

  • 首屏内容(Hero、导航)同步加载
  • 折叠线以下内容用 dynamic() + Suspense 分割
  • 纯交互组件(如 Three.js 背景、复杂表单)设置 ssr: false,避免服务端渲染无意义开销

3.5 滚动动画的性能陷阱

作品集常用滚动触发动画,但不当实现会导致主线程阻塞:

// hooks/useScrollAnimation.ts
import { useEffect, useRef } from 'react'
import gsap from 'gsap'
import { ScrollTrigger } from 'gsap/ScrollTrigger'

gsap.registerPlugin(ScrollTrigger)

export function useScrollAnimation() {
  const containerRef = useRef<HTMLDivElement>(null)

  useEffect(() => {
    const ctx = gsap.context(() => {
      gsap.from('.project-card', {
        y: 60,
        opacity: 0,
        duration: 0.8,
        stagger: 0.15,
        ease: 'power3.out',
        scrollTrigger: {
          trigger: containerRef.current,
          start: 'top 85%',
          toggleActions: 'play none none none',
        },
      })
    }, containerRef)

    return () => ctx.revert() // 清理,防止内存泄漏
  }, [])

  return containerRef
}

性能原则

  • 动画元素添加 will-change: transform, opacity,让浏览器提前创建合成层
  • 只动画 transformopacity,避免触发布局(layout)和重绘(paint)
  • 使用 GSAP 的 context()revert() 管理生命周期,组件卸载时清理 ScrollTrigger

四、关键组件示例

4.1 响应式项目网格

// components/sections/FeaturedProjects.tsx
import { ProjectCard } from '@/components/ui/ProjectCard'
import { getFeaturedProjects } from '@/lib/projects'

export function FeaturedProjects() {
  const projects = getFeaturedProjects()

  return (
    <section className="py-24 px-6">
      <div className="max-w-7xl mx-auto">
        <h2 className="text-3xl font-bold mb-12">精选项目</h2>
        <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
          {projects.map((project) => (
            <ProjectCard key={project.slug} project={project} />
          ))}
        </div>
      </div>
    </section>
  )
}

4.2 图片画廊(Lightbox)

// components/ui/Gallery.tsx
'use client'

import { useState } from 'react'
import Image from 'next/image'
import { motion, AnimatePresence } from 'framer-motion'

export function Gallery({ images }: { images: { src: string; alt: string }[] }) {
  const [selected, setSelected] = useState<number | null>(null)

  return (
    <>
      <div className="grid grid-cols-2 md:grid-cols-3 gap-4">
        {images.map((img, i) => (
          <button
            key={i}
            onClick={() => setSelected(i)}
            className="relative aspect-[4/3] overflow-hidden rounded-lg"
          >
            <Image
              src={img.src}
              alt={img.alt}
              fill
              className="object-cover hover:scale-105 transition-transform"
              sizes="(max-width: 768px) 50vw, 33vw"
            />
          </button>
        ))}
      </div>

      <AnimatePresence>
        {selected !== null && (
          <motion.div
            initial={{ opacity: 0 }}
            animate={{ opacity: 1 }}
            exit={{ opacity: 0 }}
            onClick={() => setSelected(null)}
            className="fixed inset-0 z-50 bg-black/90 flex items-center justify-center p-4"
          >
            <Image
              src={images[selected].src}
              alt={images[selected].alt}
              width={1200}
              height={800}
              className="max-w-full max-h-[90vh] object-contain"
              priority
            />
          </motion.div>
        )}
      </AnimatePresence>
    </>
  )
}

五、Core Web Vitals 调优清单

作品集网站必须关注的三个核心指标:

指标目标优化手段
LCP(最大内容绘制)< 2.5s首屏图片加 priority、使用 next/image、预加载关键资源
INP(交互到下一次绘制)< 200ms减少主线程长任务、事件委托、使用 requestIdleCallback 处理非紧急逻辑
CLS(累积布局偏移)< 0.1图片/视频设置宽高比容器、字体用 next/font、避免无尺寸插入内容

额外技巧

  • next.config.js 中启用 images.formats: ['image/avif', 'image/webp'],优先使用 AVIF
  • 使用 <link rel="preload"> 预加载首屏关键字体和 CSS
  • 部署后使用 Vercel Analytics 或 PageSpeed Insights 持续监控

六、部署方案

平台推荐度说明
Vercel⭐⭐⭐原生支持 Next.js,边缘网络、自动 HTTPS、Analytics 集成,作品集首选
Cloudflare Pages⭐⭐⭐边缘渲染、全球 CDN,适合国际访客
GitHub Pages⭐⭐纯静态导出(output: 'export'),免费但无 SSR

Vercel 部署配置

// next.config.js
const nextConfig = {
  images: {
    formats: ['image/avif', 'image/webp'],
    remotePatterns: [
      { protocol: 'https', hostname: 'cdn.sanity.io' }, // 如果使用 CMS
    ],
  },
}

module.exports = nextConfig

连接 GitHub 仓库后,每次 push 自动构建部署,并生成预览链接。


七、总结

用 Next.js 构建作品集网站,核心不是堆砌技术,而是让技术隐形——访客感受到的是流畅的加载、顺滑的滚动和精致的排版,而非背后的框架。

关键 takeaway:

  1. SSG 优先:所有项目页构建时生成,CDN 边缘缓存
  2. 图片即性能next/image + priority + placeholder="blur" 是 LCP 的救命稻草
  3. 字体零偏移next/font 自托管,告别 FOUT
  4. 懒加载非关键内容dynamic() 分割代码,Suspense 优雅降级
  5. 动画守规矩:只动 transform/opacity,用 GSAP 管理生命周期

一个加载在 1 秒内、Lighthouse 评分 95+ 的作品集,本身就是最好的技术证明。


延伸阅读