部署与优化
本章将详细介绍 Next.js 应用的部署方式和性能优化技巧。部署是应用开发周期的最后一步,选择正确的部署方式和优化策略对于用户体验至关重要。
部署选项概览
Next.js 支持多种部署方式,每种方式有其适用场景:
| 部署方式 | 特性支持 | 适用场景 |
|---|---|---|
| Node.js 服务器 | 全部 | 传统服务器部署、需要完全控制 |
| Docker 容器 | 全部 | 容器化环境、Kubernetes 集群 |
| 静态导出 | 部分 | 静态网站、CDN 托管 |
| Vercel | 全部 | 快速部署、无需运维 |
选择部署方式时,需要考虑以下因素:
- 功能需求:静态导出不支持服务端特性(如 ISR、Server Actions)
- 团队技能:Docker 和 Kubernetes 需要运维知识
- 成本预算:Vercel 提供免费套餐,但大规模应用需要付费
- 性能要求:CDN 部署可提供全球加速
Node.js 服务器部署
这是最基础的部署方式,将 Next.js 作为 Node.js 应用运行。
项目配置
确保 package.json 包含必要的脚本:
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
}
}
构建和启动
# 构建生产版本
npm run build
# 启动服务器(默认端口 3000)
npm run start
# 指定端口和主机
PORT=8080 HOSTNAME=0.0.0.0 npm run start
构建过程会:
- 编译 TypeScript 和 JSX
- 优化生产构建
- 生成静态页面和服务端路由
- 输出到
.next目录
使用 PM2 管理进程
PM2 是 Node.js 应用的进程管理器,提供自动重启、负载均衡等功能:
# 安装 PM2
npm install -g pm2
# 启动应用
pm2 start npm --name "next-app" -- start
# 查看状态
pm2 status
# 查看日志
pm2 logs next-app
# 设置开机自启
pm2 startup
pm2 save
创建 PM2 配置文件 ecosystem.config.js 进行更精细的控制:
module.exports = {
apps: [
{
name: "next-app",
script: "npm",
args: "start",
instances: "max", // 使用所有 CPU 核心
exec_mode: "cluster", // 集群模式
env_production: {
NODE_ENV: "production",
PORT: 3000,
},
env_staging: {
NODE_ENV: "production",
PORT: 3001,
},
},
],
};
# 使用配置文件启动
pm2 start ecosystem.config.js --env production
反向代理配置
使用 Nginx 作为反向代理:
# /etc/nginx/sites-available/next-app
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
}
启用 HTTPS(使用 Certbot):
# 安装 Certbot
sudo apt install certbot python3-certbot-nginx
# 获取证书
sudo certbot --nginx -d example.com -d www.example.com
Docker 部署
Docker 提供了一致的部署环境,适合容器化部署和 Kubernetes 集群。
Standalone 输出模式
首先,在 next.config.ts 中启用 standalone 输出:
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: "standalone",
};
export default nextConfig;
Standalone 模式会:
- 自动追踪依赖文件
- 只包含运行所需的 node_modules
- 生成独立的
server.js文件 - 大幅减小镜像体积
基础 Dockerfile
# 阶段 1: 依赖安装
FROM node:20-alpine AS deps
WORKDIR /app
# 安装依赖所需的系统包
RUN apk add --no-cache libc6-compat
# 复制 package 文件
COPY package.json package-lock.json* ./
# 安装依赖
RUN npm ci
# 阶段 2: 构建
FROM node:20-alpine AS builder
WORKDIR /app
# 复制依赖
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# 设置环境变量
ENV NEXT_TELEMETRY_DISABLED 1
# 构建
RUN npm run build
# 阶段 3: 运行
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV production
ENV NEXT_TELEMETRY_DISABLED 1
# 创建非 root 用户
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# 复制构建产物
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
# 切换用户
USER nextjs
# 暴露端口
EXPOSE 3000
ENV PORT 3000
ENV HOSTNAME "0.0.0.0"
# 启动服务器
CMD ["node", "server.js"]
构建和运行
# 构建镜像
docker build -t next-app .
# 运行容器
docker run -p 3000:3000 next-app
# 带环境变量运行
docker run -p 3000:3000 -e DATABASE_URL=postgresql://... next-app
多环境配置
使用 Docker Compose 管理多环境:
# docker-compose.yml
version: "3.8"
services:
app:
build:
context: .
dockerfile: Dockerfile
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- DATABASE_URL=${DATABASE_URL}
env_file:
- .env.production
restart: unless-stopped
# 可选:数据库服务
db:
image: postgres:15
environment:
POSTGRES_DB: mydb
POSTGRES_PASSWORD: password
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
# 启动服务
docker-compose up -d
# 查看日志
docker-compose logs -f app
# 停止服务
docker-compose down
优化 Docker 构建
利用构建缓存:
# 优化后的 Dockerfile
FROM node:20-alpine AS deps
WORKDIR /app
# 先复制 package 文件,利用 Docker 缓存
COPY package.json package-lock.json* ./
RUN npm ci
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# 只在代码变化时重新构建
RUN npm run build
# ... 其他步骤
使用 .dockerignore:
# .dockerignore
node_modules
.next
out
.git
.gitignore
*.md
.env*.local
Kubernetes 部署
创建 Kubernetes 部署配置:
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: next-app
spec:
replicas: 3
selector:
matchLabels:
app: next-app
template:
metadata:
labels:
app: next-app
spec:
containers:
- name: next-app
image: next-app:latest
ports:
- containerPort: 3000
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: app-secrets
key: database-url
resources:
requests:
cpu: "100m"
memory: "256Mi"
limits:
cpu: "500m"
memory: "512Mi"
livenessProbe:
httpGet:
path: /api/health
port: 3000
initialDelaySeconds: 30
periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
name: next-app-service
spec:
selector:
app: next-app
ports:
- port: 80
targetPort: 3000
type: LoadBalancer
静态导出
静态导出将应用构建为纯静态文件,可以部署到任何静态托管服务。
配置
// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: "export",
// 可选:URL 尾部斜杠
// trailingSlash: true,
// 可选:修改输出目录
// distDir: "dist",
};
export default nextConfig;
构建和部署
# 构建
npm run build
# 输出到 out 目录
# out/
# ├── index.html
# ├── 404.html
# ├── about/
# │ └── index.html
# └── _next/
# └── static/
支持的特性
静态导出支持以下特性:
服务端组件:在构建时执行,渲染为静态 HTML
export default async function Page() {
// 在构建时执行
const res = await fetch("https://api.example.com/data");
const data = await res.json();
return <main>{data.title}</main>;
}
客户端数据获取:使用 SWR 等库在客户端获取数据
"use client";
import useSWR from "swr";
const fetcher = (url: string) => fetch(url).then((r) => r.json());
export default function Page() {
const { data, error, isLoading } = useSWR(
"https://api.example.com/data",
fetcher
);
if (isLoading) return <div>加载中...</div>;
if (error) return <div>加载失败</div>;
return <div>{data.title}</div>;
}
路由处理器(GET 方法):生成静态文件
// app/api/data/route.ts
export async function GET() {
return Response.json({ name: "Lee" });
}
// 构建后生成 api/data.json 文件
图片优化:使用自定义 loader
// next.config.ts
const nextConfig: NextConfig = {
output: "export",
images: {
loader: "custom",
loaderFile: "./image-loader.ts",
},
};
// image-loader.ts
export default function cloudinaryLoader({
src,
width,
quality,
}: {
src: string;
width: number;
quality?: number;
}) {
const params = ["f_auto", "c_limit", `w_${width}`, `q_${quality || "auto"}`];
return `https://res.cloudinary.com/demo/image/upload/${params.join(",")}${src}`;
}
不支持的特性
静态导出不支持需要 Node.js 服务器的特性:
- 动态路由(没有
generateStaticParams) - 增量静态再生(ISR)
- Server Actions
- API 路由(非 GET 方法)
- Cookies、Headers
- Rewrites、Redirects
- 中间件
- 拦截路由
如果尝试使用这些特性,构建会报错。需要确保应用只使用静态导出支持的特性。
Nginx 配置
server {
listen 80;
server_name example.com;
root /var/www/out;
location / {
try_files $uri $uri.html $uri/ =404;
}
# 处理不带 .html 的请求
location /blog/ {
rewrite ^/blog/(.*)$ /blog/$1.html break;
}
error_page 404 /404.html;
}
环境变量
环境变量管理是部署的关键部分,Next.js 提供了完善的环境变量支持。
环境变量文件
Next.js 支持以下环境变量文件,按优先级从高到低:
.env.$(NODE_ENV).local- 环境特定的本地覆盖.env.local- 本地覆盖(不会被 Git 追踪).env.$(NODE_ENV)- 环境特定变量.env- 默认变量
# .env
DATABASE_URL="postgresql://localhost:5432/mydb"
# .env.development
DATABASE_URL="postgresql://localhost:5432/mydb_dev"
# .env.production
DATABASE_URL="postgresql://prod-db:5432/mydb"
# .env.local(覆盖其他文件,不提交到 Git)
DATABASE_URL="postgresql://localhost:5432/mydb_local"
服务端环境变量
默认情况下,环境变量只在服务端可用:
// 只在服务端可用
console.log(process.env.DATABASE_URL);
// 在 Server Component 中使用
export default async function Page() {
const db = await connect(process.env.DATABASE_URL);
const users = await db.users.findMany();
return <UserList users={users} />;
}
客户端环境变量
使用 NEXT_PUBLIC_ 前缀暴露变量到客户端:
# .env
NEXT_PUBLIC_API_URL="https://api.example.com"
NEXT_PUBLIC_GA_ID="UA-XXXXX-Y"
// 在客户端和服务端都可用
console.log(process.env.NEXT_PUBLIC_API_URL);
// 在 Client Component 中使用
"use client";
export default function Component() {
const apiUrl = process.env.NEXT_PUBLIC_API_URL;
return <div>API: {apiUrl}</div>;
}
NEXT_PUBLIC_ 前缀的变量会被内联到客户端 JavaScript 中。不要将敏感信息(如 API 密钥、数据库密码)放在这些变量中。
运行时环境变量
默认情况下,NEXT_PUBLIC_ 变量在构建时被内联。如果需要运行时读取:
方法一:通过 API 路由
// app/api/config/route.ts
import { NextResponse } from "next/server";
export async function GET() {
return NextResponse.json({
apiUrl: process.env.API_URL,
});
}
方法二:使用 next-runtime-env 库
npm install next-runtime-env
// app/layout.tsx
import { env } from "next-runtime-env";
export default function Layout({ children }) {
return (
<html>
<head>
<script
dangerouslySetInnerHTML={{
__html: `window.ENV = ${JSON.stringify({
API_URL: env("API_URL"),
})}`,
}}
/>
</head>
<body>{children}</body>
</html>
);
}
环境变量验证
使用 Zod 验证环境变量:
// lib/env.ts
import { z } from "zod";
const envSchema = z.object({
DATABASE_URL: z.string().url(),
NEXT_PUBLIC_API_URL: z.string().url(),
JWT_SECRET: z.string().min(32),
});
export const env = envSchema.parse(process.env);
Vercel 部署
Vercel 是 Next.js 的官方托管平台,提供零配置部署体验。
部署步骤
- 推送代码到 Git 仓库
git init
git add .
git commit -m "Initial commit"
git remote add origin https://github.com/username/my-app.git
git push -u origin main
- 连接 Vercel
- 访问 vercel.com
- 使用 GitHub/GitLab/Bitbucket 账号登录
- 点击 "Import Project"
- 选择你的 Git 仓库
- 配置项目
Vercel 会自动检测 Next.js 项目,通常无需额外配置:
- Framework Preset: Next.js
- Root Directory: ./
- Build Command:
npm run build - Output Directory:
.next
- 部署
点击 "Deploy",Vercel 会:
- 安装依赖
- 构建项目
- 部署到全球 CDN
- 分配一个
.vercel.app域名
环境变量配置
在 Vercel 项目设置中配置环境变量:
- 进入项目 Dashboard
- 点击 Settings → Environment Variables
- 添加变量,选择环境(Production/Preview/Development)
也可以使用 CLI:
# 安装 Vercel CLI
npm install -g vercel
# 登录
vercel login
# 添加环境变量
vercel env add DATABASE_URL production
# 输入值: postgresql://...
# 拉取环境变量到本地
vercel env pull .env.local
预览部署
每次推送代码到非主分支,Vercel 会创建一个预览部署:
- 生成唯一的预览 URL
- 用于测试和代码审查
- 不影响生产环境
自定义域名
- 在项目设置中点击 Domains
- 添加自定义域名
- 配置 DNS 记录
- 等待 SSL 证书自动配置
部署保护
Vercel 提供部署保护功能:
- 密码保护:需要密码才能访问
- Vercel Authentication:需要 Vercel 账号登录
- Trusted IPs:只允许特定 IP 访问
性能优化
性能优化是提升用户体验的关键,Next.js 提供了多种优化工具。
图片优化
使用 next/image 组件自动优化图片:
import Image from "next/image";
export default function Hero() {
return (
<div className="relative w-full h-[400px]">
<Image
src="/hero.jpg"
alt="Hero image"
fill
priority // 首屏图片优先加载
className="object-cover"
sizes="100vw"
/>
</div>
);
}
关键属性说明:
priority:预加载图片,适用于首屏关键图片fill:填充父容器,需要父容器有定位sizes:响应式图片尺寸提示,帮助生成正确的 srcsetplaceholder="blur":加载时显示模糊占位符
响应式图片:
<Image
src="/product.jpg"
alt="Product"
width={800}
height={600}
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
/>
远程图片配置:
// next.config.ts
const nextConfig: NextConfig = {
images: {
remotePatterns: [
{
protocol: "https",
hostname: "example.com",
pathname: "/images/**",
},
],
},
};
字体优化
使用 next/font 自动优化字体加载:
// app/layout.tsx
import { Inter, Playfair_Display } from "next/font/google";
const inter = Inter({
subsets: ["latin"],
display: "swap", // 避免 FOIT(Flash of Invisible Text)
variable: "--font-inter",
});
const playfair = Playfair_Display({
subsets: ["latin"],
variable: "--font-playfair",
});
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="zh-CN" className={`${inter.variable} ${playfair.variable}`}>
<body className={inter.className}>{children}</body>
</html>
);
}
在 CSS 中使用:
/* globals.css */
:root {
--font-inter: "Inter", sans-serif;
--font-playfair: "Playfair Display", serif;
}
h1, h2, h3 {
font-family: var(--font-playfair);
}
body {
font-family: var(--font-inter);
}
本地字体:
import localFont from "next/font/local";
const myFont = localFont({
src: [
{
path: "./fonts/MyFont-Regular.woff2",
weight: "400",
},
{
path: "./fonts/MyFont-Bold.woff2",
weight: "700",
},
],
variable: "--font-my-font",
});
代码分割和懒加载
动态导入组件:
import dynamic from "next/dynamic";
// 懒加载组件
const DynamicChart = dynamic(() => import("@/components/Chart"), {
loading: () => <ChartSkeleton />,
ssr: false, // 禁用 SSR(适用于纯客户端组件)
});
export default function Dashboard() {
const [showChart, setShowChart] = useState(false);
return (
<div>
<button onClick={() => setShowChart(true)}>显示图表</button>
{showChart && <DynamicChart />}
</div>
);
}
动态导入外部库:
"use client";
import { useState } from "react";
export default function Search() {
const [results, setResults] = useState([]);
const handleSearch = async (query: string) => {
// 动态导入 fuse.js(模糊搜索库)
const Fuse = (await import("fuse.js")).default;
const fuse = new Fuse(data, { keys: ["title", "content"] });
setResults(fuse.search(query));
};
return (
<input
onChange={(e) => handleSearch(e.target.value)}
placeholder="搜索..."
/>
);
}
使用 React.lazy 和 Suspense:
"use client";
import { lazy, Suspense } from "react";
const HeavyComponent = lazy(() => import("@/components/Heavy"));
export default function Page() {
return (
<Suspense fallback={<LoadingSpinner />}>
<HeavyComponent />
</Suspense>
);
}
脚本优化
使用 next/script 优化第三方脚本加载:
import Script from "next/script";
export default function Page() {
return (
<>
{/* 关键脚本:在页面交互前加载 */}
<Script
src="/critical-script.js"
strategy="beforeInteractive"
/>
{/* 分析脚本:页面加载后执行 */}
<Script
src="https://analytics.example.com/script.js"
strategy="afterInteractive"
onLoad={() => {
console.log("分析脚本已加载");
}}
/>
{/* 非关键脚本:浏览器空闲时加载 */}
<Script
src="/chat-widget.js"
strategy="lazyOnload"
/>
<main>页面内容</main>
</>
);
}
策略说明:
| 策略 | 执行时机 | 适用场景 |
|---|---|---|
beforeInteractive | 页面交互前 | 关键脚本,如用户认证 |
afterInteractive | 页面加载后 | 分析、追踪脚本 |
lazyOnload | 浏览器空闲时 | 聊天组件、广告 |
预加载优化
链接预加载:
import Link from "next/link";
// 默认预加载视口内的链接
<Link href="/about">关于</Link>
// 禁用预加载(适用于不常用的链接)
<Link href="/admin" prefetch={false}>
管理后台
</Link>
编程式预加载:
"use client";
import { useRouter } from "next/navigation";
export default function ProductCard({ product }: { product: Product }) {
const router = useRouter();
// 鼠标悬停时预加载
const handleMouseEnter = () => {
router.prefetch(`/products/${product.id}`);
};
return (
<Link
href={`/products/${product.id}`}
onMouseEnter={handleMouseEnter}
>
{product.name}
</Link>
);
}
流式渲染
使用 Suspense 实现流式渲染:
import { Suspense } from "react";
export default function Dashboard() {
return (
<div className="grid gap-4">
{/* 这两个区域独立加载 */}
<Suspense fallback={<StatsSkeleton />}>
<Stats />
</Suspense>
<Suspense fallback={<ChartSkeleton />}>
<Chart />
</Suspense>
</div>
);
}
// 独立获取数据
async function Stats() {
const stats = await getStats(); // 可能很慢
return <div>{stats.total} 个用户</div>;
}
async function Chart() {
const data = await getChartData(); // 可能很慢
return <ChartComponent data={data} />;
}
缓存策略
合理的缓存策略可以显著提升性能。
数据缓存
fetch 缓存选项:
// 默认不缓存(Next.js 15)
const data = await fetch("https://api.example.com/data");
// 启用缓存
const data = await fetch("https://api.example.com/data", {
cache: "force-cache",
});
// 定时重新验证(ISR)
const data = await fetch("https://api.example.com/data", {
next: { revalidate: 60 }, // 每 60 秒重新验证
});
// 标签重新验证
const data = await fetch("https://api.example.com/data", {
next: { tags: ["posts"] },
});
页面级缓存配置:
// app/blog/page.tsx
// 静态页面
export const revalidate = false;
// 定时重新验证
export const revalidate = 3600; // 每小时
// 动态渲染
export const dynamic = "force-dynamic";
// 强制静态
export const dynamic = "force-static";
静态资源缓存
配置浏览器缓存:
// next.config.ts
const nextConfig: NextConfig = {
headers: async () => [
{
source: "/images/:path*",
headers: [
{
key: "Cache-Control",
value: "public, max-age=31536000, immutable",
},
],
},
{
source: "/_next/static/:path*",
headers: [
{
key: "Cache-Control",
value: "public, max-age=31536000, immutable",
},
],
},
],
};
监控与分析
构建分析
使用 @next/bundle-analyzer 分析包大小:
npm install -D @next/bundle-analyzer
// next.config.ts
import analyzer from "@next/bundle-analyzer";
const withBundleAnalyzer = analyzer({
enabled: process.env.ANALYZE === "true",
});
const nextConfig: NextConfig = {
// 配置...
};
export default withBundleAnalyzer(nextConfig);
# 运行分析
ANALYZE=true npm run build
性能监控
使用 Web Vitals:
// app/layout.tsx
import { Analytics } from "@vercel/analytics/react";
export default function RootLayout({ children }) {
return (
<html>
<body>
{children}
<Analytics />
</body>
</html>
);
}
自定义报告:
// app/layout.tsx
import { SpeedInsights } from "@vercel/speed-insights/next";
export default function RootLayout({ children }) {
return (
<html>
<body>
{children}
<SpeedInsights />
</body>
</html>
);
}
错误追踪
集成 Sentry 进行错误追踪:
npm install @sentry/nextjs
// sentry.client.config.ts
import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
tracesSampleRate: 1.0,
environment: process.env.NODE_ENV,
});
最佳实践总结
部署选择
- 新项目/快速迭代:选择 Vercel
- 企业级应用:选择 Docker + Kubernetes
- 静态内容网站:选择静态导出 + CDN
- 需要完全控制:选择 Node.js 服务器
性能优化清单
-
图片优化
- 使用
next/image组件 - 为首屏图片添加
priority - 配置合适的
sizes属性
- 使用
-
字体优化
- 使用
next/font加载字体 - 设置
display: "swap"避免 FOIT
- 使用
-
代码分割
- 使用
dynamic懒加载大型组件 - 动态导入外部库
- 使用
-
缓存策略
- 为静态数据启用缓存
- 为动态数据设置合理的重新验证时间
- 使用标签进行精确的缓存失效
-
脚本优化
- 使用
next/script管理第三方脚本 - 根据重要性选择正确的策略
- 使用
安全检查清单
- 不要将敏感信息暴露在
NEXT_PUBLIC_变量中 - 使用 HTTPS
- 配置安全的 Cookie(httpOnly, secure, sameSite)
- 定期更新依赖
- 启用 CSP(内容安全策略)
小结
本章我们学习了:
- 多种部署方式的特点和选择
- Node.js 服务器部署和 PM2 管理
- Docker 部署的最佳实践
- 静态导出的配置和限制
- 环境变量的管理和安全
- Vercel 部署流程
- 图片、字体、代码分割等性能优化
- 缓存策略的配置
- 监控与分析工具的使用
练习
- 使用 Docker 部署一个 Next.js 应用
- 配置静态导出并部署到 CDN
- 分析应用包大小,优化大型依赖
- 实现一个带缓存策略的博客列表页