性能与可观测 — CodeRoadMap性能与可观测
懒加载与 OpenTelemetry。
性能与可观测
这一课你将能做到
- 用 Lighthouse 和
next/bundle-analyzer 诊断性能瓶颈
- 用动态导入
next/dynamic 实现组件级代码分割和懒加载
- 用
next/script 控制第三方脚本的加载时机,避免阻塞主线程
- 用 Vercel Speed Insights 和 Google Analytics 监控 Core Web Vitals
- 理解 LCP、FID、CLS 三个核心指标,并有针对性地优化
为什么要学
性能差的网站直接等于收入损失:亚马逊统计每慢 100ms 转化率降 1%,Google 搜索排名把 Core Web Vitals 纳入算法。Next.js 给你了很多优化工具,但如果你不知道 LCP 是什么、<Script> 的 strategy 怎么选、bundle 里藏了多少重复代码,这些工具就白搭。
人话:性能优化不是「能跑就行」,而是「让服务器少干活、让用户少等待、让浏览器少卡顿」。监控则是「装摄像头」,让你知道哪里慢了、卡了、崩了。
先建立图像
Core Web Vitals 三剑客:
├── LCP (Largest Contentful Paint) ← 首屏最大元素多久渲染(≤2.5s 优)
├── FID (First Input Delay) / INP ← 用户第一次交互延迟(≤100ms 优)
└── CLS (Cumulative Layout Shift) ← 页面布局偏移(≤0.1 优)
Next.js 性能优化工具箱:
├── next/image → 自动压缩、WebP、懒加载
├── next/font → 自托管字体,防布局偏移
├── next/dynamic → 按需加载组件
├── next/script → 控制脚本加载策略
├── Suspense → 流式渲染,减少白屏
└── 分包分析 → 找到 bundle 里的"大象"
跟着做
步骤 1:安装 bundle-analyzer
npm install -D @next/bundle-analyzer
next.config.ts:
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
// ... 其他配置
};
export default nextConfig;
package.json 加脚本:
{
"scripts": {
"analyze": "ANALYZE=true next build"
}
}
Windows 用户用:
{
"scripts": {
"analyze": "set ANALYZE=true&&next build"
}
}
执行 npm run analyze,构建完成后会自动打开浏览器,展示服务端和客户端的 bundle 组成。
步骤 2:动态导入和代码分割
app/page.tsx:
import dynamic from 'next/dynamic';
import { Suspense } from 'react';
// 默认懒加载,不打包进首屏 bundle
const HeavyChart = dynamic(() => import('./HeavyChart'), {
ssr: false, // 不在服务端渲染(如果有 DOM 操作)
loading: () => <p>图表加载中…</p>,
});
export default function Home() {
return (
<main>
<h1>首页</h1>
<p>上方内容立刻显示</p>
<Suspense fallback={<p>图表区域加载中…</p>}>
<HeavyChart />
</Suspense>
</main>
);
}
:
app/HeavyChart.tsx
'use client';
export default function HeavyChart() {
return <div style={{ height: 400, background: '#f5f5f5' }}>这是一个很重的图表组件</div>;
}
HeavyChart 只有在进入视口或组件需要时才会加载,首屏 bundle 里不包含它。
步骤 3:next/script 控制第三方脚本
import Script from 'next/script';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="zh-CN">
<head>
<Script
src="https://analytics.example.com/script.js"
strategy="lazyOnload" // 页面加载完成后才加载
data-site-id="12345"
/>
</head>
<body>{children}</body>
</html>
);
}
beforeInteractive:加载 HTML 之前,阻塞渲染(首屏关键脚本)
afterInteractive:加载 HTML 之后,不阻塞渲染(默认,推荐用于大多数脚本)
lazyOnload:空闲时加载(分析脚本、聊天机器人)
worker:在 Web Worker 中加载(实验性,减少主线程阻塞)
步骤 4:图片优化实战
import Image from 'next/image';
export default function Gallery() {
return (
<div>
<Image
src="/hero.jpg"
alt="首屏大图"
width={1200}
height={600}
priority // 预加载,计入 LCP
quality={80}
sizes="100vw"
/>
<Image
src="/gallery-1.jpg"
alt="画廊图 1"
width={400}
height={300}
loading="lazy" // 默认就是 lazy,显式写更清晰
placeholder="blur"
blurDataURL="data:image/jpeg;base64,..."
/>
</div>
);
}
<Image> 自动:priority 的图片用 <link rel="preload"> 预加载;lazy 的图片进入视口才加载;sizes 帮助生成正确的 srcset。
步骤 5:Vercel Speed Insights
npm install @vercel/speed-insights
import { SpeedInsights } from '@vercel/speed-insights/next';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
{children}
<SpeedInsights />
</body>
</html>
);
}
部署到 Vercel 后,在 Vercel Dashboard → Speed Insights 面板查看 LCP、INP、CLS 等真实用户数据。
步骤 6:减少 CLS
- 图片没给尺寸 → 用
<Image> 或 width/height
- 字体加载导致文字跳动 → 用
next/font 的 font-display: swap
- 动态内容插入 → 给容器预留最小高度
export default function CommentSection() {
return (
<div style={{ minHeight: 200 }}>
{/* 评论组件加载前,容器已经占好位置 */}
<Suspense fallback={<p>评论加载中…</p>}>
<CommentList />
</Suspense>
</div>
);
}
讲清楚要点
- LCP(Largest Contentful Paint):首屏最大可见元素(通常是 hero 图或 h1 标题)的渲染时间。优化手段:图片预加载、服务端渲染关键内容、CDN 加速。
- INP(Interaction to Next Paint):用户交互到下一帧渲染的时间,替代了旧指标 FID。优化手段:减少主线程任务、用
requestIdleCallback 处理低优先级工作、减少长任务。
- CLS(Cumulative Layout Shift):页面元素位置偏移的累计值。优化手段:给图片/iframe 固定尺寸、用
next/font 防字体跳动、给动态内容预留空间。
- 代码分割不是越多越好:每个 chunk 都有 HTTP 请求开销。合理粒度:路由级自动分割 + 手动拆分大组件。
next/dynamic 和 React.lazy 区别:next/dynamic 是 Next.js 包装版,支持 SSR 选项、自定义加载组件、prefetch。推荐用 next/dynamic。
- 第三方脚本是最大的性能杀手:Google Tag Manager、百度统计、客服机器人、广告——每个都可能阻塞主线程 100ms+。用
next/script 的 lazyOnload 推迟非关键脚本。
你可能会懵的地方
-
bundle-analyzer 怎么读?
看饼图里最大的几个块,通常是 node_modules 里的库。找重复依赖(比如 lodash 和 lodash-es 同时存在)、找没用到的大库(如 moment.js 可用 dayjs 替代)、找被全量引入的库(如 import _ from 'lodash' 应改为按需引入)。
-
ssr: false 后 SEO 还收得到内容吗?
不会。ssr: false 的组件只在浏览器渲染,HTML 里是空的。如果组件内容对 SEO 重要,不要用 ssr: false。
-
<Script> 的 beforeInteractive 能用吗?
不推荐。它放在 <head> 最前面,阻塞渲染,严重影响性能。只有「不加载它页面就不可用」的脚本才用。
-
INP 和 FID 什么关系?
FID 只测第一次交互的输入延迟,不够全面。INP 测所有交互到下一帧的延迟,更能反映真实体验。Google 已经用 INP 替代 FID。
-
动态导入的 loading 组件只在网络慢时显示吗?
不,只要有加载过程就显示。即使是本地快加载,loading 组件也会一闪而过。如果想完全避免闪烁,加 ssr: true 让服务端先渲染骨架屏。
错法对照
| 错法 | 问题 | 正解 |
|---|
| 所有组件都动态导入 | 分块太多,请求开销大 | 只拆 >100KB 的大组件或非常用页 |
<script> 标签直接放在 JSX 里 | 无法控制加载策略 | 用 <Script strategy="lazyOnload"> |
| 图片不给 width/height | CLS 飙升 | 用 <Image> 或手动给尺寸 |
把分析脚本放 beforeInteractive | 阻塞首屏 | 用 afterInteractive 或 lazyOnload |
| bundle 分析完不优化 | 分析完了不行动 | 按大小排序,逐个分析替代方案 |
卡点急救
- bundle 分析后找不到问题:关注 gzipped size 不是 raw size;关注 dependencies(第三方)不是 source(自己代码)。
- 动态导入报错
window is not defined:用 ssr: false 关闭服务端渲染,或在组件内部判断 typeof window !== 'undefined'。
- 第三方脚本加载后报错:检查脚本依赖的 DOM 元素是否已渲染。
afterInteractive 保证 DOM 已就绪,但组件卸载时可能脚本还在执行。
- LCP 超过 4 秒:先确认 LCP 元素是什么(Lighthouse 会标注),如果是图片,加
priority + 压缩尺寸;如果是文字,确认不是 JS 动态渲染的。
- CLS 很高但不知道哪里在跳:用 Chrome DevTools → Performance → 勾选 Layout Shift Regions,紫色高亮区域就是偏移位置。
面试常问
-
Core Web Vitals 三个指标是什么?怎么优化?
LCP(首屏最大元素渲染时间)→ 图片预加载、SSR、CDN;INP(交互延迟)→ 减少长任务、用 Web Worker、debounce;CLS(布局偏移)→ 固定尺寸、预加载字体、预留空间。
-
怎么减少首屏 bundle 体积?
① 路由级自动代码分割;② next/dynamic 手动分割大组件;③ tree-shaking 未使用的导出;④ 替换大库(moment→dayjs、lodash→es-toolkit);⑤ 用 next/font 代替外部字体请求。
-
next/dynamic 的 ssr: false 和 ssr: true 区别?
ssr: true(默认)时组件在服务端和客户端都渲染,HTML 包含内容,SEO 友好;ssr: false 时组件只在客户端渲染,HTML 为空,适合依赖浏览器 API 的库(如 chart.js、编辑器)。
-
怎么监控生产环境性能?
① Vercel Speed Insights(真实用户数据,自动上报);② Google Analytics 4 的 Core Web Vitals 报告;③ Sentry 的错误和性能追踪;④ Lighthouse CI 自动化审计。
自测三问
<Image priority> 和普通 <Image> 在加载行为上有什么不同?
next/dynamic 和 React.lazy 相比多了哪些能力?
Script strategy="lazyOnload" 适合什么类型的脚本?不适合什么?
小结一句话
性能优化是「测出来、拆出去、懒下来」三步走:用 bundle-analyzer 找到大象,用 next/dynamic 拆出去,用 next/script 懒下来,用 Speed Insights 持续监控——不是一次性工作,而是持续交付的一部分。
延伸