返回博客
用 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:首屏可见图片必须加,否则会被懒加载延迟,拖累 LCPplaceholder="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 缓存的 HTMLgenerateMetadata:每个页面生成独立的<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,让浏览器提前创建合成层 - 只动画
transform和opacity,避免触发布局(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:
- SSG 优先:所有项目页构建时生成,CDN 边缘缓存
- 图片即性能:
next/image+priority+placeholder="blur"是 LCP 的救命稻草 - 字体零偏移:
next/font自托管,告别 FOUT - 懒加载非关键内容:
dynamic()分割代码,Suspense 优雅降级 - 动画守规矩:只动 transform/opacity,用 GSAP 管理生命周期
一个加载在 1 秒内、Lighthouse 评分 95+ 的作品集,本身就是最好的技术证明。
延伸阅读: