第 6 章 · 性能 / 部署 / 工程化:图片、流式渲染、JS 体积、build 产物、部署
本章主线一句话:性能优化的本质是回答三件事——"发了多少数据""渲染花多久""要不要重新渲染";Next 给的答案分别是 next/image(少发图)、流式渲染(快出内容)、缓存/静态化(少渲染)与 RSC 边界(少发 JS)。
6.0 为什么需要这一章(动机)
前面五章讲透了渲染模型、路由、RSC、缓存、Server Actions。但面试最后往往会落到一句:"你实际怎么让一个 Next 应用更快?"
这一章把性能问题收敛成三个可操作的维度:
| 维度 | 问题 | Next 的解法 |
|---|---|---|
| 网络载荷 | 发了多少数据? | next/image(图片)、next/dynamic + RSC 边界(JS) |
| 渲染速度 | 首屏要等多久? | 流式渲染(Streaming)、缓存 |
| 渲染频率 | 要不要重新渲染? | SSG/ISR、四层缓存、revalidatePath |
记忆锚点:性能 = 少发(JS/图片) + 快出(流式/缓存) + 少算(静态化)。
6.1 少发数据:next/image 图片优化
图片通常是页面里最大的资源。原生 <img> 会整张加载,不做懒加载、不控尺寸。
next/image 内置优化:
import Image from "next/image"
<Image
src="/photo.jpg"
alt="照片"
width={800} // 声明宽高,防止布局偏移(CLS)
height={600}
loading="lazy" // 默认:视口外图片延迟加载
placeholder="blur" // 占位(可选,配 blurDataURL)
/>
关键点(面试常考):
- 声明
width/height:为图片预留空间,避免加载后页面跳动(CLS)。 - 默认懒加载:视口外的图片不阻塞首屏。
- 远程图片:域名必须在
next.config.ts的images.remotePatterns登记,否则报错。 - 本项目:
next.config设了unoptimized: true(跳过优化服务,用原始 URL),生产可开启优化服务 / CDN 获得更佳压缩。
6.1b 远程图片不知道宽高怎么办
next/image 需要 width/height(或 fill)来预留空间防 CLS,但远程图片宽高未知。按场景选:
方案一:fill + 容器定尺寸(最常用)——让容器决定占多大,图片填满它:
{/* 父容器必须 relative + 有明确尺寸 */}
<div className="relative h-60 w-full overflow-hidden">
<Image
src="https://远程图片.jpg"
alt="描述"
fill
className="object-cover"
sizes="(max-width: 768px) 100vw, 50vw" // 告诉浏览器不同宽度下占多少
/>
</div>
方案二:width/height + CSS 自适应(自由流式插图):
<Image
src="https://远程图片.jpg"
alt="描述"
width={1600} // 给一个合理上限
height={900}
style={{ width: "100%", height: "auto" }} // 覆盖为自适应,不按给定宽高拉伸
/>
方案三:服务端先取真实尺寸(零 CLS)——服务端组件里解析图片宽高再传给 next/image:
async function getImageSize(url: string) {
// 用 image-size 之类库解析,或后端返回时带尺寸
return { width: 1600, height: 900 }
}
export default async function Page() {
const { width, height } = await getImageSize("https://远程.jpg")
return <Image src="https://远程.jpg" alt="" width={width} height={height} />
}
| 场景 | 推荐 |
|---|---|
| 图片撑满已知尺寸的容器(卡片/横幅/封面) | 方案一 fill |
| 自由流式插图(正文图) | 方案二 width/height + auto |
| 要零 CLS、尺寸重要 | 方案三 服务端取真实尺寸 |
6.1c 瀑布流(不等高图片)
瀑布流图片宽度固定(=列宽)、高度不等,关键难点是必须按宽高比预留精确高度,否则整列会乱跳(CLS)。绕不开"知道宽高比"。
正解:数据源带宽高 + aspectRatio + fill(零 CLS)
// 数据源返回每张图:{ url, width, height }
function MasonryCard({ img }: { img: { url: string; width: number; height: number } }) {
return (
<div
className="relative w-full overflow-hidden"
style={{ aspectRatio: `${img.width} / ${img.height}` }} // 按真实比例预留高度
>
<Image
src={img.url}
alt=""
fill
sizes="300px" // 列宽
className="object-cover"
loading="lazy"
/>
</div>
)
}
aspectRatio按真实宽高比预留空间 → 图片加载后完全不跳动。- 尺寸来源:① 数据源/后端返回(最推荐);② 服务端用库解析;③ 客户端
onLoad拿到(会有跳动,仅兜底)。
对比普通布局 vs 瀑布流:
| 普通流式图 | 瀑布流 | |
|---|---|---|
| 宽度 | 撑满容器(fill) | = 列宽(固定) |
| 高度 | CSS 自适应(height:auto) | 必须按宽高比预留(aspectRatio) |
| 知道宽高吗 | 可不精确 | 必须精确(否则整列跳动) |
| 核心手段 | fill / width+height+auto | aspectRatio + fill |
一句话:远程图片不知道宽高,用
fill(容器定尺寸)或width/height + auto(自适应);瀑布流则必须用aspectRatio按比例预留高度,尺寸靠数据源给。
6.2 快出内容:流式渲染(Streaming)
问题:SSR 如果等最慢的组件算完才返回整页,首屏 TTFB 会被最慢的那块拖住。
解法:用 <Suspense> 把慢速区块包住,页面分块流式下发——不慢的部分先返回,慢区块就绪后再填充。
import { Suspense } from "react"
export default function Page() {
return (
<div>
{/* 立即渲染:不等慢数据 */}
<Header />
{/* 慢区块:先显示骨架,就绪后流式填充 */}
<Suspense fallback={<Skeleton />}>
<SlowSection />
</Suspense>
</div>
)
}
// SlowSection —— 服务端组件,await 慢数据(如耗时查询)
async function SlowSection() {
const data = await slowQuery()
return <div>{data}</div>
}
关键点:
- 每个
Suspense边界是一个"流式填充节点",就绪后逐块发给浏览器。 - 收益:首屏 TTFB 更快、用户先看到已有内容/骨架、感知更快。
loading.tsx(第 2 章约定文件)= 整个页面默认包了一层 Suspense。- 流式渲染适合"页面大部分快、个别区块慢"的场景。
- 坑(重要):页面必须是"动态的"才能流式渲染。如果页面是静态预渲染(
○),构建时await已执行完,用户看到的是"算好的完整页",没有流式效果。需要export const dynamic = "force-dynamic"强制动态,每次请求重新渲染、Suspense 边界逐个流式下发。(本项目实验已加,见lab/perf/streaming。)
6.3 少发 JS:next/dynamic 动态导入
问题:如果首屏就把所有组件(含图表库、编辑器、地图等大依赖)的 JS 都发下去,首屏 JS 体积爆炸。
解法:把不首屏需要的重量级组件,用 next/dynamic 拆成独立 chunk,按需加载。
import dynamic from "next/dynamic"
// HeavyWidget 不会进首屏 bundle,点击时才加载
const HeavyWidget = dynamic(() => import("./heavy-widget"), {
ssr: false, // 可选:纯客户端依赖时设为 false
loading: () => <p>加载中…</p>,
})
关键点:
next/dynamic让组件成为独立 JS chunk,不进首屏。- 适用:图表、编辑器、地图、只在用户操作后才出现的重组件。
- 与
React.lazy类似,但还支持ssr开关和loading占位。 - 更强的思路:如果组件能改成服务端组件(不发 JS),比动态导入更优(承接第 3 章)——动态导入是"降级 JS",RSC 是"根本不发 JS"。
6.3b 包体积优化与分包
先区分两个概念:分包(Code Splitting)是手段,包体积(Bundle Size)是目标——分包 = 把代码拆成多个 chunk 按需加载,让浏览器总下载量(尤其首屏)更小。
关键认知:Next 默认就分包了,你不用手动做。
Next 默认的 chunk 划分:
├─ 主 bundle(框架 + 公共代码)—— 所有页面共享,首屏必下
├─ 每个路由一个 page chunk —— 访问到哪个页才下哪个
├─ 共享依赖 chunk —— 多个页面都用的大库单独拆一份
└─ 动态导入 chunk —— next/dynamic 拆出来的,点击才下
Next 内置分包机制:
- 路由级分包:每个
page.tsx一个独立 chunk,只加载当前路由需要的那份(默认)。 - 依赖自动拆分:React / 框架等共享依赖抽成公共 chunk,被多个路由复用(默认)。
- 动态导入(
next/dynamic):手动再拆一层,大组件/库按需加载。
分包方式一览:
| 方式 | 作用 | Next 内置 |
|---|---|---|
| 路由级分包 | 每个页面一个 chunk | ✅ 默认 |
| 公共依赖拆分 | React/框架抽公共 chunk | ✅ 默认 |
next/dynamic | 手动拆大组件/库,点击才下 | ✅ 默认 |
React.lazy + Suspense | 客户端侧懒加载 | 需手动 |
| 手动 vendor 拆分 | 把大依赖单独拆 | ⚙️ 需配置 |
包体积优化的实操手段:
- 按需引入依赖(tree-shaking,最有效)
// 差:整个库打包进来
import _ from "lodash"
// 好:只引需要的函数(可被 tree-shaking 剔除未用)
import { debounce } from "lodash"
// 差:整个组件库
import { Button } from "antd"
// 好:按需引(babel-plugin-import 或引子路径)
-
用
next/dynamic拆大组件——图表/编辑器/地图等不进首屏的动态导入。 -
避免重复打包——共享逻辑抽公共模块,别让同一大库在多个 chunk 各打一份。
-
用 bundle analyzer 定位大依赖(关键工具)
npm i -D @next/bundle-analyzer
生产构建后可视化每个 chunk 大小,一眼看出"哪个库最大、该不该拆"。
- Tree-shaking 靠 ESM——用
import { x }(ESM)而非require(CJS),构建器才能剔除未用代码。
一句话:Next 默认做了路由级 + 公共依赖分包;你要做的包体积优化是"按需引依赖、动态拆大组件、用 analyzer 定位大依赖"——核心是"首屏只发首屏需要的,其他按需拆走"。
6.4 减少 JS 的根本:RSC 边界(面试加分)
next/dynamic 只是"把 JS 拆开按需发"。更根本的是让组件根本不发 JS:
- 纯展示、读服务端数据的组件 → 做成服务端组件(默认,不发 JS)。
- 只有需要交互/浏览器 API 的部分 → 才用客户端组件(
'use client')。 - 把"客户端边界"往下压,客户端 JS 就越小。
尽量往上(服务端组件,零 JS)
┌─────────────┐
│ 页面骨架 │ ← 服务端,不发 JS
├─────────────┤
│ 列表(读数据)│ ← 服务端,不发 JS
├─────────────┤
│ 交互按钮 │ ← 客户端边界('use client'),才发 JS
└─────────────┘
客户端 JS 只包含最下面这一小块
一句话:动态导入解决"少发首屏 JS",RSC 边界解决"根本不发 JS"——后者更优,是 Next 的性能杀手锏。
6.5 少渲染:缓存 / 静态化(承接第 4 章)
性能不只是"快出",还有"少算":
- SSG/ISR:构建时渲染一次,之后直接返回缓存 HTML(服务器零渲染成本)。
- 四层缓存(第 4 章):数据缓存 + 完整路由缓存让"同一个页面"不重复渲染。
revalidatePath/revalidateTag:只在需要时让缓存失效。
面试连贯题:"SSR 性能差怎么办?" → 先问"这页需要 SEO 吗、内容因人而异吗"→ 能静态就 SSG/ISR,不能就流式渲染 + 缓存。
6.6 读 next build 产物(实操必备)
pnpm build 后日志会打印每个路由的渲染标记(末尾有图例):
| 标记 | 含义 |
|---|---|
○ | Static:静态预渲染(构建时渲染一次) |
● | SSG:用 generateStaticParams 的静态 HTML |
ƒ | Dynamic:按需服务端渲染 |
用 build 产物做性能定位:
- 看某页是不是静态:日志找它的标记,
○/●静态、ƒ动态。 - 静态页产物在
.next/server/app/.../*.html;动态页每次请求才渲染。 - 找 JS 体积:看
.next/static/chunks/*.js,或 bundle 分析插件定位大依赖。 - 判断"为什么变动态":回看第 1 章 1.6(动态 API / fetch 不缓存 / force-dynamic)和第 4 章(缓存失效)。
6.7 部署
| 部署方式 | 特点 | 适用 |
|---|---|---|
| Vercel(官方推荐) | 零配置、边缘函数、预览部署、自动 HTTPS | 个人/中小项目,Next 官方平台 |
| Node 自托管 | pnpm build && pnpm start,用 PM2/Docker 托管 | 需要自己控制服务器、内网部署 |
| Docker | 官方镜像 node + 运行 build 产物 | 容器化、K8s 部署 |
| 边缘(Edge) | 中间件/部分路由跑在边缘节点 | 全球加速、低延迟 |
部署注意事项:
- 构建产物是可复现的:
.next由 build 生成,部署时重新 build(不在服务器上乱改)。 - 环境变量:服务端环境变量在 build 时注入,客户端
NEXT_PUBLIC_*会打进 bundle,勿放密钥。 - 缓存:静态资源上 CDN,配合
Cache-Control;ISR 页面可被 CDN 缓存。 - 本项目的 next-sitemap:
pnpm build后postbuild会生成 sitemap/robots(构建产物,注意 git 提交范围)。
6.8 Next 16 新特性(面试加分)
基于本项目实测(Next 16.1.7):
- Turbopack 默认:Next 16 默认用 Turbopack 做构建(本项目 build 日志显示
Next.js 16.1.7 (Turbopack)),编译更快。 - React Compiler:
next.config的reactCompiler: true开启(本项目已开启),自动记忆化组件,减少不必要的重渲染。 use cache/ Cache Components:新一代缓存指令,允许同一路由混合静态/动态(第 4 章提到)。revalidateTag签名变化:Next 16 起需要第二个参数profile(缓存生命周期档位),形如revalidateTag("tag", "default")(第 4 章实验已踩坑)。- 构建/运行时对动态 API、缓存失效的判断更严格,行为更可预期。
面试讲"新特性"别堆名词,讲清楚"解决什么问题":Turbopack 解决构建慢,React Compiler 解决重渲染,
use cache解决"同页混合静态/动态"。
6.9 代码实验
位置:src/app/lab/perf/,入口 /lab/perf。
| 实验 | 目录 | 验证目标 |
|---|---|---|
| 图片优化 | image/(next/image vs 原生 img) | 尺寸防 CLS、懒加载、占位 |
| 流式渲染 | streaming/(Suspense + 骨架屏) | 慢区块不阻塞整页,先出骨架再填充 |
| 动态导入 | dynamic/(next/dynamic 懒加载) | 重量组件不进首屏,点击才加载独立 chunk |
| 读 build 产物 | build/(标记说明 + 本项目实测) | 理解 ○/●/ƒ、.next 结构 |
观察方法:
streaming:Network 把网速调慢,看页面先出内容/骨架,再流式填充(不整页等待)。dynamic:首次点"加载"时 Network 出现新的 JS chunk。build:pnpm build后对照日志标记理解每个路由。- 全部性能实验,最终结合
pnpm build && pnpm start看真实标记与产物。
6.10 面试自测题
<details> <summary>1. 让一个 Next 应用更快,你会从哪三件事入手?</summary>网络载荷(少发 JS/图片,用 next/image + next/dynamic + RSC 边界)、渲染速度(流式渲染 + 缓存,快出内容)、渲染频率(SSG/ISR + 四层缓存,少算)。一句话:少发、快出、少算。
</details> <details> <summary>2. next/image 相比原生 img 有什么好处?</summary>自动处理宽高(防布局偏移 CLS)、默认懒加载(视口外不阻塞首屏)、可配占位、可接入优化服务/CDN 做压缩。远程图片域名需在 next.config 的 remotePatterns 登记。
</details> <details> <summary>3. 什么是流式渲染?解决什么问题?</summary>用 Suspense 把慢速区块包住,页面分块流式下发:不慢的部分先返回,慢区块就绪后再填充。解决 SSR 首屏被最慢组件拖住、TTFB 变慢的问题,让用户先看到已有内容/骨架。
</details> <details> <summary>4. next/dynamic 是做什么的?</summary>把重量级组件拆成独立 JS chunk,按需加载,不进首屏 bundle。适用图表/编辑器/地图等大依赖。比 React.lazy 多了 ssr 开关和 loading 占位。
</details> <details> <summary>5. next/dynamic 和 RSC 边界有什么区别?哪个更优?</summary>next/dynamic 是"把 JS 拆开按需发",RSC 边界是"根本不发 JS"(服务端组件不发 bundle)。RSC 更优——能把组件做成服务端组件的,就别让它发 JS;动态导入是"降级"手段。
</details> <details> <summary>6. build 日志里 ○、●、ƒ 分别代表什么?</summary>○ = Static(静态预渲染,构建时渲染一次);● = SSG(用 generateStaticParams 的静态 HTML);ƒ = Dynamic(按需服务端渲染)。看标记能判断页面是否静态化、性能如何。
</details> <details> <summary>7. 一个页面"为什么变成了动态(ƒ)"?怎么排查?</summary>因为它用了动态 API(cookies/headers/searchParams)、fetch 默认不缓存、或显式 force-dynamic。排查看 build 标记 + 回看第 1 章 1.6 动态 API 列表和第 4 章缓存机制。
</details> <details> <summary>8. Next 能部署到哪些环境?</summary>Vercel(官方推荐,零配置+边缘+预览)、Node 自托管(pnpm build && pnpm start,配 PM2/Docker)、容器化(Docker/K8s)、边缘节点。选型看控制力、性能、运维成本。
</details> <details> <summary>9. 部署时环境变量有什么要注意的?</summary>服务端环境变量在 build 时注入;客户端 NEXT_PUBLIC_* 会打进 bundle,不能放密钥。生产敏感信息必须走服务端环境变量或密钥管理服务。
</details> <details> <summary>10. Next 16 有哪些值得提的新特性?</summary>Turbopack 默认(构建更快)、React Compiler(自动记忆化,减少重渲染)、use cache / Cache Components(同页混合静态动态)、revalidateTag 签名变化(需第二个参数 profile)。讲的时候要说明"解决什么问题"。
</details>6.11 常见追问(加分项)
- "SSR 性能差怎么优化?":先判断页面是否需要 SEO/是否因人而异——能静态就 SSG/ISR,不能就流式渲染(先出骨架)+ 缓存(数据/路由缓存)+"少发 JS"(RSC 边界 + 动态导入)。
- "图片优化具体做了什么?":尺寸预设防 CLS、懒加载、占位、转 WebP/AVIF 压缩、接 CDN。核心是"少发 + 不发多余的"。
- "为什么动态导入不如 RSC?":动态导入仍会下载那段 JS(只是延后),RSC 让服务端组件根本不进 bundle——彻底不发。
- "next build 和 next start 的关系?":build 生成生产产物(含静态页 HTML、chunk、优化),start 用这些产物启动生产服务器。部署 = 构建产物 + 运行环境。
- "build 标记对性能意味着什么?":○/● 是"构建时已算好",请求零渲染成本;ƒ 是"每次请求现算",有服务器开销。性能问题先看有多少 ƒ、能不能转成 ○。
- "Next 和纯前端相比,性能瓶颈在哪?":主要在服务端渲染成本(SSR/动态)和 Node 单线程。缓解靠静态化、缓存、流式、边缘渲染;重计算交给 Java/Python(承接第 5 章 5.11 架构视角)。
六章到这里结束。核心主线回顾:渲染模型(1)→ 路由(2)→ 组件边界(3)→ 数据缓存(4)→ 写操作(5)→ 性能工程化(6)。面试前按"主线一句话 + 自测题"过一遍即可。
