跳到主要内容

部署与优化

本章将详细介绍 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. 实现一个带缓存策略的博客列表页