实战经验

一次博客性能优化的完整记录

以我的技术博客(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%。

--------------------------------

效果与复盘

优化前后对比:

指标优化前优化后提升
FCP2.8s320ms88%
LCP4.2s680ms84%
TTI5.1s920ms82%
CLS0.340.0294%
总资源大小4.7MB890KB81%
Lighthouse 得分3196

这次经历留下的思考框架:

  • 先数据,后猜测。 我没有一上来就改代码,而是先用 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)

暂无评论,快来发表第一条评论吧