跳到主要内容

部署与优化

本章将详细介绍 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

构建过程会:

  1. 编译 TypeScript 和 JSX
  2. 优化生产构建
  3. 生成静态页面和服务端路由
  4. 输出到 .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 支持以下环境变量文件,按优先级从高到低:

  1. .env.$(NODE_ENV).local - 环境特定的本地覆盖
  2. .env.local - 本地覆盖(不会被 Git 追踪)
  3. .env.$(NODE_ENV) - 环境特定变量
  4. .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 的官方托管平台,提供零配置部署体验。

部署步骤

  1. 推送代码到 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
  1. 连接 Vercel
  • 访问 vercel.com
  • 使用 GitHub/GitLab/Bitbucket 账号登录
  • 点击 "Import Project"
  • 选择你的 Git 仓库
  1. 配置项目

Vercel 会自动检测 Next.js 项目,通常无需额外配置:

  • Framework Preset: Next.js
  • Root Directory: ./
  • Build Command: npm run build
  • Output Directory: .next
  1. 部署

点击 "Deploy",Vercel 会:

  • 安装依赖
  • 构建项目
  • 部署到全球 CDN
  • 分配一个 .vercel.app 域名

环境变量配置

在 Vercel 项目设置中配置环境变量:

  1. 进入项目 Dashboard
  2. 点击 Settings → Environment Variables
  3. 添加变量,选择环境(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
  • 用于测试和代码审查
  • 不影响生产环境

自定义域名

  1. 在项目设置中点击 Domains
  2. 添加自定义域名
  3. 配置 DNS 记录
  4. 等待 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:响应式图片尺寸提示,帮助生成正确的 srcset
  • placeholder="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,
});

最佳实践总结

部署选择

  1. 新项目/快速迭代:选择 Vercel
  2. 企业级应用:选择 Docker + Kubernetes
  3. 静态内容网站:选择静态导出 + CDN
  4. 需要完全控制:选择 Node.js 服务器

性能优化清单

  1. 图片优化

    • 使用 next/image 组件
    • 为首屏图片添加 priority
    • 配置合适的 sizes 属性
  2. 字体优化

    • 使用 next/font 加载字体
    • 设置 display: "swap" 避免 FOIT
  3. 代码分割

    • 使用 dynamic 懒加载大型组件
    • 动态导入外部库
  4. 缓存策略

    • 为静态数据启用缓存
    • 为动态数据设置合理的重新验证时间
    • 使用标签进行精确的缓存失效
  5. 脚本优化

    • 使用 next/script 管理第三方脚本
    • 根据重要性选择正确的策略

安全检查清单

  1. 不要将敏感信息暴露在 NEXT_PUBLIC_ 变量中
  2. 使用 HTTPS
  3. 配置安全的 Cookie(httpOnly, secure, sameSite)
  4. 定期更新依赖
  5. 启用 CSP(内容安全策略)

小结

本章我们学习了:

  1. 多种部署方式的特点和选择
  2. Node.js 服务器部署和 PM2 管理
  3. Docker 部署的最佳实践
  4. 静态导出的配置和限制
  5. 环境变量的管理和安全
  6. Vercel 部署流程
  7. 图片、字体、代码分割等性能优化
  8. 缓存策略的配置
  9. 监控与分析工具的使用

练习

  1. 使用 Docker 部署一个 Next.js 应用
  2. 配置静态导出并部署到 CDN
  3. 分析应用包大小,优化大型依赖
  4. 实现一个带缓存策略的博客列表页