从v1到v2,我是如何优化小破站的?
上个月把博客迭代了一个大版本,顺便整理一下我都做了哪些优化,以及踩了什么坑。
关于 SEO 的系统性介绍可以看如何优化网站的SEO,这篇只讲实操
本站原架构采用 Vue 3 SPA + Spring Boot,典型的单页应用。
大概的问题有这些:
-
首屏载入速度太慢,甚至因此导致了 FCP 测试超时

-
页面 SEO 元数据不全,不利于搜索引擎理解
-
单页应用,页面全靠 JavaScript 动态注入,爬虫读取不到内容
1. 添加 robots.txt 与 Sitemap
User-agent: *
Allow: /
Allow: /api/user/articles/sitemap.xml
Disallow: /admin/
Disallow: /api/
Sitemap: https://blog.hazenix.top/api/user/articles/sitemap.xml
后端新增 /sitemap.xml 端点,查询已发布文章的 slug + 更新时间,组织为 XML Sitemap,把固定页面和动态页面都纳入 sitemap 中
2. 性能优化(提升网站响应速度与 Core Web Vitals)
LCP 视角
LCP (Largest Contentful Paint,最大内容渲染)衡量页面最大可见元素的渲染完成时间,合格线 ≤ 2.5s。以下优化主要围绕减少首屏资源体积、缩短关键渲染路径。
添加骨架屏
// 优化前
<div id="app"></div>
修复之前,浏览器拿到的 HTML 里面啥也没有。必须等 Vue 的 JS 下载完、跑起来、渲染出 DOM,用户才能看到第一个像素。所以 Lightroom 测试显示为 NO_FCP(没有任何内容渲染)
// 优化后
// 在 `<div id="app">` 内加了纯 HTML + inline CSS 的骨架屏,浏览器 JS 执行前就立即渲染
<div id="app">
<div style="min-height:100vh; display:flex; align-items:center; justify-content:center;">
<h1>Hazenix's Blog</h1>
<p>Only those who keep walking light up the long road of time</p>
</div>
</div>
修复后,浏览器拿到 HTML 的瞬间就能把这些字画出来——0ms 延迟,纯 HTML + inline CSS,不需要等任何 JS。
减包 + 懒加载
优化前前端打包后的 dist 目录太大,首屏要下载的东西太多,拖慢了 LCP。针对打包体积和加载速度做了一系列优化:
视频转移到 OSS:原树洞页面的 sunrise.mp4 背景视频打包在 dist 里,占了 34MB。改为上传到阿里云 OSS,在元素进入视口后才开始下载。dist 体积从 45MB 降到 15MB
图片转 WebP:头像 avatar.jpg(53KB → 25KB)、微信二维码 Hazenix.png(223KB → 44KB),用 sharp 转为 WebP,总共减少约 200KB。
移除 Font Awesome CDN:30 处图标全部迁移到 Element Plus 自带的图标库(@element-plus/icons-vue),消除了一次外部 CSS 请求。
Element Plus 按需导入
原来 main.js 里是全局导入 import ElementPlus from 'element-plus',CSS 和所有组件全部打包。改为 unplugin-vue-components + unplugin-auto-import 按需导入,CSS 体积从 389KB 降到约 60KB
Brotli 预压缩
使用 vite-plugin-compression 在构建时预先生成 .br 和 .gz 文件,Nginx 直接返回压缩后的资源,不用实时压缩,响应速度更快 传输体积进一步减少
例如
TableOfContentschunk:1685KB → 400KB(Brotli 压缩后),element-pluschunk:878KB → 233KB。
列表 API 响应减重
文章列表接口原来返回所有文章的完整正文 HTML,单次响应 396KB。优化之后只查标题、时间、统计数据,396KB → 5KB,减小 98.7%
// 修复前:select * from article(含正文)
List<ArticleDetailVO> list = articleService.getArticleList(query);
// 修复后:select 仅必要字段(不含 content)
List<ArticleShortVO> list = articleService.getArticleShortList(query);
封面图使用 OSS 缩略
文章列表/推荐卡片展示的封面图直接用了 OSS 原图,一张截屏 PNG 动辄 1.4MB。首页 5 篇文章就拉 7MB 图片流量,将 LCP 拖慢到了 6s
优化后直接使用 OSS 提供的缩略图 (列表里只需要 400px 宽的小缩略图),降低带宽占用
异步组件拆分 —— 把重型依赖踢出首屏
ArticleDetail.vue 里 MarkdownRenderer、CommentList、TableOfContents 都是静态 import,导致 Vite 把它们全部打进同一个 chunk——markdown-it(200KB)、mermaid(包含 dagre/d3/katex,1.2MB)、highlight.js(300KB)一起挤在 1.67MB 的 chunk 里,首页明明不需要这些,也跟着下载。
优化后重型组件异步加载,用户点进文章页才下载markdown 渲染、代码高亮、mermaid 画图等重型依赖
第三方脚本延迟 + 关键资源预加载
GA4 和 Umami 统计脚本的 async/defer 在模拟 3G 下仍然会和主 JS 竞争首屏带宽。把它们全部移到 window.load 事件之后才初始化:
<script>
function loadThirdParty() {
// Umami + GA4 统统在 load 之后才加载
var u = document.createElement('script');
u.defer = true;
u.src = 'https://monitor.hazenix.top/script.js';
// ...
}
window.addEventListener('load', loadThirdParty);
</script>
同时在 <head> 里加 LCP 关键图片的预加载和 fetchpriority="high",让头像和 JS 并行下载:
<link rel="preload" as="image" href="/avatar.webp" fetchpriority="high">
CLS(布局偏移)
CLS 衡量页面加载过程中视觉内容的意外偏移,合格线 ≤ 0.1。
使用 CSS Containment 防布局偏移
Vue 挂载替换预渲染内容时,如果动态数据(文章列表、热门文章)与预渲染时的快照不同,容器高度变化会产生 CLS。给关键区域加 min-height 和 contain: layout style:
<div class="lg:col-span-2" style="min-height:400px;contain:layout style;">
<!-- 文章列表 -->
</div>
<div style="min-height:250px;contain:layout style;">
<!-- 推荐区域 -->
</div>
CSS contain: layout style 告诉浏览器这个区域内的布局变化不会影响外部元素,避免级联重排。
效果:
/home的 CLS 从 0.165 降到 0。
Nginx 安全加固 + 缓存策略收紧
安全头和缓存策略也是 PageSpeed “Best Practices” 评分项,之前一直缺了几项。
Content-Security-Policy:限制浏览器只能加载可信来源的资源,防止 XSS 和数据注入攻击:
add_header Content-Security-Policy "default-src 'self';
script-src 'self' 'unsafe-inline' https://monitor.hazenix.top
https://www.googletagmanager.com https://www.google-analytics.com;
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
img-src 'self' https://blog-hazenix-top.oss-cn-heyuan.aliyuncs.com data:;
connect-src 'self' https://blog.hazenix.top/api https://www.google-analytics.com;
font-src 'self' https://fonts.gstatic.com data:;
frame-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'" always;
解释:
script-src只允许本站 + Google Analytics / Umami 的脚本style-src允许本站 + Google Fonts 样式表img-src允许本站 + OSS 图片 + data URI(base64 内联图)object-src 'none'禁止 Flash / Java 插件(现代浏览器已不需要)base-uri 'self'防止<base>标签劫持
同时加了 Permissions-Policy 禁止地理位置、麦克风、摄像头(博客用不到)。
缓存策略细化:之前的图片缓存一刀切 30 天。assets/ 目录下的资源(JS/CSS/图片)带内容哈希,文件名变了自动失效,可以放心开 1 年 immutable:
# 带哈希 → 1 年强缓存
location ~* ^/assets/.*\.(js|css)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# 根目录静态资源(无哈希)→ 30 天
location ~* ^/[^/]+\.(jpg|png|webp)$ {
expires 30d;
add_header Cache-Control "public";
}
3. 结构化资料标记 & 优化 Title / Meta Description
优化前只有一行 document.title = ...。
优化后在所有页面统一注入:title、description、keywords、OG 标签、Twitter Card、canonical URL、JSON-LD 结构化数据。
| 页面 | 结构化数据类型 |
|---|---|
| 首页 | WebSite + SearchAction(启用 Google 站内搜索框) |
| 文章页 | Article + BreadcrumbList(面包屑导航) |
| 分类/标签/关于/友链/留言板/历史 | 各页面独立标题 + 描述 |
404 页面额外设置了
robots: 'noindex',防止被搜索引擎索引。
4. 设置图片 ALT 属性
MarkdownRenderer 渲染的 <img> 标签自动添加 loading="lazy"(延迟加载,图片滚到视口附近才开始下载,不抢首屏带宽。)和 decoding="async"(异步解码,图片在后台线程解码,不阻塞主线程渲染),同时确保 alt 属性存在(帮助搜索引擎理解图片内容)。显著改善多图长文章场景下的 LCP
5. 解决单页应用的爬虫读取问题
单页应用的爬虫读取问题 的讲解参考上一篇文章的“## 9. JavaScript”部分
预渲染 + 增量更新
受 grtblog v2 架构 启发——“SSR 渲染从用户请求时转移到数据变更时”。
预渲染:构建时用 Puppeteer (无头浏览器)把所有页面(文章、分类、标签、静态页)渲染成完整 HTML,去掉 <script> 标签后存入 dist/prerender/。Nginx 直接返回预渲染的静态 HTML
增量更新:使用一个独立的 prerender-server(Node.js HTTP 服务),Spring Boot 端对文章/分类/标签增删改查 后通过 webhook 通知它**重渲染受影响页面。**不需要全量重建,只刷新变了的那几个页面。
测试优化前后的SEO
测试前后对比
Google Search Console 的数据要等 1-2 周才有,但以下工具的测试结果都是即时的。
优化前:


优化后:


优化前后总览
| 维度 | 优化前 | 优化后 |
|---|---|---|
| robots.txt | 不存在 | 可访问 |
| Sitemap 数据 | 返回空 | XML Sitemap,含 90+ 页面 |
| 文章页 meta description | 无 | 自动填入文章摘要 |
| 文章页 OG 标签 | 无 | og:title, og:image, og:url 齐全 |
| 文章页结构化数据 | 无 | Article + BreadcrumbList |
| 首页结构化数据 | 无 | WebSite + SearchAction |
| 其他页面 SEO 元数据 | 6 个页面缺失 | 全部接入 setSEO() |
| 404 页面 HTTP 状态码 | 200(软 404) | 404 |
| 图片加载 | 无优化 | loading="lazy" + decoding="async" |
| 爬虫内容获取 | 空壳 HTML | 预渲染静态 HTML(Puppeteer) |
| 普通用户首屏 | 空壳骨架屏等 Vue 加载 | 预渲染完整 HTML(瞬时 LCP) |
| 分享按钮桌面端 | “分享失败” | 复制链接成功 |
| dist 打包体积 | 45MB | 15MB(减 67%) |
| 图片格式 | PNG/JPG 共 ~276KB | WebP 共 ~70KB(减 75%) |
| Font Awesome | CDN 外部请求 | 已移除(Element Plus 图标替代) |
| Element Plus | 全量导入 389KB CSS | 按需导入 ~60KB |
| 异步组件拆分 | 无(1.67MB TOC chunk) | MarkdownRenderer 等按需加载 |
| 文章列表 API | 396KB(含正文 HTML) | 5KB(仅标题/时间,减 98.7%) |
| 封面图 | 原图 1.4MB/张(PNG) | OSS 缩放 14KB/张(WebP,减 99%) |
| 首页总流量 | 7,646KB | 241KB(减 97%) |
| Brotli 传输压缩 | 无 | 构建时预生成 .br(再减 60%) |
| 第三方脚本 | 首屏与 JS 竞争带宽 | load 事件后延迟初始化 |
| CSP 安全头 | 无 | Content-Security-Policy + Permissions-Policy |
| 静态资源缓存 | 图片 30d,无区分 | 哈希资源 1y immutable,SW no-cache |
| PageSpeed Mobile | 53 | 99 |
| PageSpeed Desktop | 98 | 99 |
| PageSpeed Best Practices | — | 100 |
| PageSpeed LCP (Mobile) | 6.2s | 1.8s |
| PageSpeed TBT (Mobile) | 340ms | 0ms |
| PageSpeed CLS | 0.165 | 0 |
| Lighthouse SEO 分数 | ~50-60 | 90-100 |
| Google Rich Results | 未检测到 | Article + BreadcrumbList |
评论 (0)
暂无评论,快来抢沙发吧!