以我的技术博客(Next.js + Nginx + PostgreSQL + Vercel部署)为主角,记录一次从"首页打开4.2秒"到"680毫秒"的完整优化过程。核心不在于某个神技,而在于排查思路:如何用浏览器开发者工具一层层剥开问题,定位到真正的瓶颈。
背景
我的博客是一个 Next.js 静态站,部署在 Vercel 上,内容通过 Markdown 渲染,数据库用的是 PlanetScale(MySQL 兼容),缓存层没有。文章数量不多,大约 120 篇,月访问量 3 万左右。
平时没觉得有什么,直到有一天我在手机上打开博客,加载转圈转了将近五秒。那一刻我才意识到:慢的不是网络,是网站本身。
我打开 Chrome DevTools,Network 面板,Lighthouse 跑了一遍——结果让我沉默。
问题出现
Lighthouse 报告首页 Performance 得分:31分。
详细数据:* FCP(首次内容绘制):2.8s
- LCP(最大内容绘制):4.2s
- TTI(可交互时间):5.1s
- CLS(累积布局偏移):0.34
- 总请求数:87
- 总资源大小:4.7MB(其中图片 3.2MB)
我第一反应是"网络问题"。换了几台设备、几个网络环境,结果都一样。慢是真实的,不是我的错觉。
排查过程
第一层:Network 面板看请求
我把所有请求按大小排序,发现几个明显的问题:
1. 图片占了 3.2MB,而且全是原始尺寸。
我博客里有几十篇文章,每篇配了 2-3 张截图。这些图片是我直接用 VS Code 截的图,未经任何压缩,直接放到博客里。有一张截图是 2400×1600 的 PNG,原始大小 3.8MB。
我在页面上看到它,实际显示区域只有 600×400。浏览器在渲染之前,要先下载这 3.8MB 的数据。
我当时在想: 图片加载慢,是不是 CDN 的问题?我检查了一下,Vercel 的 Edge Network 本身没问题,但图片没有经过任何优化处理,原图直出。
2. 字体文件加载了 4 个,总大小 680KB。
我用的是 Google Fonts 的 Noto Sans SC,为了支持中文,加载了 regular、medium、bold、medium italic 四个字重。而且没有加 display=swap,导致文字渲染期间页面空白。
我当时判断错了: 我以为字体不是主要问题,因为 680KB 在整体资源中不算最大。但 Lighthouse 明确指出,字体阻塞了文字渲染,FCP 延迟了 800ms。
3. 首屏渲染了 3 个非关键 JS 包,总大小 1.2MB。
Next.js 的 `<em>next/static/chunks</em>` 里有几个大文件,其中一个是 pages/app.js 的 polyfill,另一个是我误引入的 dayjs 全量包(我只用了一个 format 方法,却引入了整个库)。
我当时的困惑: 我的博客是静态站,为什么会有这么重的 JS?后来发现,Next.js 默认会把所有页面的 JS 打包到一起,而我没有做代码分割。
第二层:Waterfall 看时序
我把 Network 面板切到 Waterfall 视图,发现一个关键问题:
关键渲染路径被阻塞了。
0ms ── 请求 HTML
800ms ── HTML 到达,开始解析
900ms ── 发现 <link rel="stylesheet">,阻塞渲染
1100ms ── CSS 到达
1200ms ── 发现 <script>,阻塞解析
1800ms ── JS bundle 到达(1.2MB)
2000ms ── JS 执行完毕,开始渲染
2800ms ── FCP我当时意识到: 问题不在单个资源的大小,而在于资源的加载顺序和阻塞关系。CSS 和 JS 都在阻塞渲染,而图片又占了大部分带宽。
第三层:Database 查询
我本来以为问题在前端,但 Lighthouse 报告里有一个"Eliminate render-blocking resources"的建议,我处理完之后,FCP 只从 2.8s 降到了 2.1s。
还有 2 秒的差距。我去查了后端日志,发现每次首页请求,都会执行一个全表扫描:
sql-- 获取最新文章列表
SELECT * FROM posts ORDER BY created_at DESC LIMIT 10这个查询本身不慢,但问题在于:每次请求都执行,没有缓存。
我当时的判断: 数据库不是主要瓶颈,但值得优化。
第四层:CLS 布局偏移
Lighthouse 给了一个 CLS 0.34 的警告。我仔细看了一下,发现是图片没有设置宽高属性,导致图片加载时页面布局发生跳动。
html<!-- 问题代码 -->
<img src="/images/screenshot.png" alt="截图" />
<!-- 修复后 -->
<img
src="/images/screenshot.png"
alt="截图"
width="600"
height="400"
loading="lazy"
/>这个改动很简单,但 CLS 从 0.34 降到了 0.02。
解决方案
优化一:图片压缩 + WebP 转换
我用了 sharp 库在构建时压缩图片,并将 PNG 转换为 WebP:
javascript// next.config.js
const withImages = require('next-images');
module.exports = withImages({
images: {
formats: ['image/avif', 'image/webp'],
deviceSizes: [640, 750, 828, 1080, 1200],
imageSizes: [16, 32, 48, 64, 96],
},
});效果:3.2MB 的图片资源降到了 420KB,压缩率 87%。
优化二:字体优化
html<!-- 修复前 -->
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC" rel="stylesheet">
<!-- 修复后 -->
<link
rel="preload"
href="/fonts/noto-sans-sc.woff2"
as="font"
type="font/woff2"
crossorigin
>
<style>
@font-face {
font-family: 'Noto Sans SC';
src: url('/fonts/noto-sans-sc.woff2') format('woff2');
font-display: swap;
}
</style>本地托管字体,自举加载,避免 Google Fonts 的网络延迟。同时用 font-display: swap 让文字先渲染,字体加载完再替换。
效果:字体阻塞时间从 800ms 降到了 120ms。
优化三:代码分割
javascript// 动态导入非关键组件
const Comments = dynamic(() => import('@/components/Comments'), {
loading: () => <p>加载中...</p>,
ssr: false,
});
// 移除全量 dayjs,改用 date-fns(tree-shakeable)
import { format } from 'date-fns';效果:首屏 JS 从 1.2MB 降到了 340KB。
优化四:数据库查询缓存
javascript// 在 Next.js 中加一层内存缓存
const cache = new Map();
export async function getLatestPosts() {
const cacheKey = 'latest_posts';
const ttl = 5 * 60 * 1000; // 5分钟
const cached = cache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < ttl) {
return cached.data;
}
const posts = await db.query(`
SELECT id, title, slug, created_at
FROM posts
ORDER BY created_at DESC
LIMIT 10
`);
cache.set(cacheKey, { data: posts, timestamp: Date.now() });
return posts;
}效果:数据库查询从每次 180ms 降到了 0ms(缓存命中)。
优化五:启用 Gzip/Brotli 压缩
Nginx 配置:
nginxgzip on;
gzip_types text/plain text/css application/json application/javascript text/xml;
gzip_min_length 1000;
brotli on;
brotli_types text/plain text/css application/json application/javascript;效果:所有文本资源压缩率约 70%。
--------------------------------
效果与复盘
优化前后对比:
| 指标 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| FCP | 2.8s | 320ms | 88% |
| LCP | 4.2s | 680ms | 84% |
| TTI | 5.1s | 920ms | 82% |
| CLS | 0.34 | 0.02 | 94% |
| 总资源大小 | 4.7MB | 890KB | 81% |
| Lighthouse 得分 | 31 | 96 | — |
这次经历留下的思考框架:
- 先数据,后猜测。 我没有一上来就改代码,而是先用 Lighthouse 和 DevTools 拿到客观数据。很多性能问题,直觉是错的。
- 关键渲染路径优先。 我花了 60% 的时间在处理阻塞渲染的资源(CSS、JS、字体),这些是"最先影响用户感知"的问题。
- 图片是静态站的最大敌人。 我的博客 68% 的带宽浪费在图片上。压缩、格式转换、懒加载,这三步是必须做的。
- 缓存是最被低估的优化。 数据库查询缓存只加了 20 行代码,但让每次请求的数据库耗时从 180ms 降到了 0。
【关键代码附录】
next.config.js 完整配置:
javascriptconst withImages = require('next-images');
module.exports = withImages({
images: {
formats: ['image/avif', 'image/webp'],
deviceSizes: [640, 750, 828, 1080, 1200],
imageSizes: [16, 32, 48, 64, 96],
domains: ['localhost', 'myblog.com'],
},
webpack: (config) => {
config.module.rules.push({
test: /\.svg$/,
use: ['@svgr/webpack'],
});
return config;
},
});Nginx 完整配置:
nginxserver {
listen 80;
server_name myblog.com;
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
brotli on;
brotli_comp_level 6;
brotli_types text/plain text/css application/json application/javascript;
location / {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# 静态资源长期缓存
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
proxy_pass http://localhost:3000;
expires 1y;
add_header Cache-Control "public, immutable";
}
}【写作后记】
优化博客的过程,让我重新理解了"性能优化"这件事——它不是一个技术点,而是一套思维方法:先测量,再假设,再验证。我见过太多开发者一上来就改代码,改完发现没效果,再改,再没效果。循环往复,问题没解决,信心先没了。
如果你也遇到了网站慢的问题,我的建议是:先跑一次 Lighthouse,把报告贴在桌上,然后从分数最低的那一项开始。 不要试图一次性解决所有问题,一个一个来,每个都有数据反馈,你会知道哪一步是有效的。
性能优化是一场马拉松,不是一锤子买卖。但好消息是,每一步都有正反馈。
评论 (0)
请先登录后再发表评论
暂无评论,快来发表第一条评论吧