一次例行测速,发现博客首屏要 6 秒。拆包、按需加载、nginx 调优三板斧下去,首屏传输量降了 93%,二次访问近乎秒开。
在线地址:https://blog.ishare.cool/(本文说的就是它自己)
序言
博客重构之后,今天顺手测了下访问速度,结果有点难看:首页那个 app.js 足足 1.73MB(gzip 后),在我这台机器上下载要 5、6 秒。服务端响应明明很快(TTFB 才 200ms),问题全在前端资源上。
于是做了一次完整优化:前端拆包 → Vue 全按需加载 → nginx 缓存与 TLS 调优 → 镜像发布,全程有数据对照。最终首页 JS 传输量从 1.73MB 降到 58KB,降幅 97%。这篇文章把整个过程和踩的坑记录下来。
先测速:问题定位
优化之前先看数据,用 curl 把每个环节的耗时拆开:
请求链路: 浏览器 ──► nginx(443) ──► Go(qinblog:8080) ──► MySQL / Redis
实测基线 (优化前)
═══════════════════════════════════════════════════════
HTML TTFB ~200ms ✓ 服务端没问题
TLS 握手 偶发 3.4~4.9s ✗ 有异常
app.js (单包) 1.73MB gzip ✗✗✗ 罪魁祸首
app.js 下载耗时 5.5 ~ 6.3s ✗ 带宽被单包打满
静态资源缓存头 无 ✗ 二次访问重复下载
静态资源 Set-Cookie 有 ✗ 资源也穿过会话中间件
折算一下服务器出口带宽:1.73MB ÷ 5.5s ≈ 315KB/s(约 2.5Mbps),这就是小带宽服务器,字节数就是时间。结论很清楚:
优化优先级
──────────────────────────────────────────────
① 减少首屏必须下载的字节数 (拆包/按需加载)
② 让二次访问不再重复下载 (浏览器强缓存)
③ 压缩与握手开销 (gzip/TLS 调优)
1.73MB 里装了什么
Vite 默认把静态 import 的依赖全打进入口。查了下 app.js 的依赖图,好家伙:
app.js 单包 1.73MB (gzip)
┌──────────────────────────────────────────────────┐
│ highlight.js 全语言包 (~190 种语言) [大头] │
│ EasyMDE + CodeMirror (Markdown 编辑器) [大头] │
│ @vueform/multiselect (标签多选) │
│ inline-attachment (拖拽上传) │
│ mermaid / KaTeX 的触发逻辑 │
│ Vue 3 运行时 (含编译器) │
│ jquery + popper + bootstrap │
│ Toc 目录组件 + 3 个后台表单组件 │
└──────────────────────────────────────────────────┘
▲
│ 问题是:这些只有特定页面才用得到
│
首页:只用 bootstrap 导航交互 (其余全白下载)
文章页:多一个 Toc + 代码高亮 (编辑器白下载)
发文页:才真正需要编辑器全家桶 (唯一全量页面)
一个只展示文章列表的首页,却背着 Markdown 编辑器和 190 种语言的高亮包跑。这就是典型的"入口贪吃"。
优化一:Vite 拆包,重依赖全按需加载
思路一句话:谁用谁加载,入口只留地基。
改动清单
- highlight.js:不再
import hljs from 'highlight.js'(全语言包),新建 hljs-loader.js,页面存在代码块时才动态加载 core + 24 种常用语言; - EasyMDE / inline-attachment / 三个表单组件:从入口静态引入改为动态 import,仅当页面出现对应组件标签时才加载;
- vite.config.js 增加 manualChunks,把 jquery/popper/bootstrap 拆成 vendor-base、Vue 拆成 vendor-vue,独立长缓存。
优化前:一锅端 优化后:分餐制
───────────────────── ──────────────────────────────
index.html index.html
│ │
▼ ├──► app.js (6KB, 路由胶水)
app.js [1.73MB 全部加载] ├──► vendor-base (jquery/bs)
│
└──► 按需动态 chunk:
├─ hljs core+24语言 ◄─ 有代码块才加载
├─ mermaid ◄─ 有图表才加载
├─ katex ◄─ 有公式才加载
└─ EasyMDE/选择组件 ◄─ 仅发文页加载
有一个约束要注意:所有 CSS import 必须留在入口。因为 Go 侧渲染 Vite 标签时只输出入口关联的 CSS,动态 chunk 的 CSS 不会自动注入 HTML,拆出去样式就丢了。
一个容易忽略的细节:window.hljs
EasyMDE 的预览高亮(codeSyntaxHighlighting)依赖全局 window.hljs。hljs 改成异步加载后,编辑器初始化时必须先等它就绪,所以 SimpleMdeComponent 的 mounted 改成了 async:
async mounted() {
// 预览代码高亮依赖 window.hljs,先确保已加载
await loadHljs()
this.easymde = new EasyMDE({ ... })
}
第一轮效果
首页必须加载的 JS (gzip)
══════════════════════════════════════════
优化前 app.js 单包 1.73MB ████████████████████
优化后 app 6.8KB
+ vendor-base 174KB ~120KB █▌
──────────────────────────────────────────
降幅:约 93%
浏览器端到端验证:文章页 mermaid 图表、KaTeX 公式、代码高亮全部正常,发文页编辑器可用,控制台零报错。
优化二:Vue 也按需,首页一个字节都不多给
第一轮做完还剩一个尾巴:vendor-vue(177KB)仍然被入口静态引入,首页没有 Vue 挂载点却也照下载不误。
本项目的 Vue 是"撒点式"使用:页面内联脚本设置 window.__vueOptions = { el, data },入口检测到才挂载。既然如此,Vue 运行时本身也可以等检测到再加载:
async function mountVue(opts) {
// 仅存在挂载点的页面(文章详情/发文页)才拉取 vue 运行时
const { createApp } = await import('vue')
const app = createApp({ ... })
// 各组件按页面实际出现的标签动态加载
if (document.querySelector('toc')) {
const { default: Toc } = await import('./components/Toc.vue')
app.component('toc', Toc)
}
if (document.querySelector('simple-mde-component, ...')) {
// 编辑器三组件 ...
}
app.mount(opts.el)
}
各页面最终加载的 JS 变成这样:
页面 加载的 JS
────────────────────────────────────────────────────────
首页 app + vendor-base (~58KB gzip)
文章详情页 + vendor-vue + Toc + hljs/mermaid/katex (有什么加载什么)
发文页 + vendor-vue + EasyMDE + 选择组件 (唯一的全量页面)
首页再降约 170KB。两轮下来:
首页 JS (gzip) 演进
══════════════════════════════════════════════════
原始单包 1.73MB ██████████████████████████████
第一轮拆包 ~120KB ██
Vue 按需 ~58KB █
──────────────────────────────────────────────────
累计降幅 97%
优化三:nginx 层——缓存、压缩与握手
前端瘦完了,轮到链路。原配置有三个问题:静态资源没有缓存头(二次访问重复下载)、穿过 Go 应用还带着 Set-Cookie、TLS 握手偶发抽风。
核心配置
# Vite 带 hash 产物:一年 immutable 强缓存(优化核心)
location /build/assets/ {
proxy_pass http://qinblog_backend;
proxy_hide_header Cache-Control;
proxy_hide_header Set-Cookie; # 静态资源无需会话 cookie
add_header Cache-Control "public, max-age=31536000, immutable" always;
add_header Strict-Transport-Security "max-age=16070400;includeSubDomains;preload" always;
access_log off;
}
# HTML/API:不缓存,保证发版后拿到最新 hash 引用
location / {
proxy_pass http://qinblog_backend;
proxy_http_version 1.1; # upstream keepalive 的前提
proxy_set_header Connection "";
...
add_header Cache-Control "no-cache, must-revalidate";
}
配套还有:upstream keepalive(减少 nginx→Go 重复建连)、ssl_session_cache(TLS 会话复用)、OCSP stapling(缩短证书验证往返)、显式 gzip(level 6,覆盖 js/css/json/svg)。
immutable 敢用一年的前提是 Vite 产物文件名带内容 hash:内容一变文件名就变,HTML 指向新文件,旧文件的缓存自然作废。这也决定了强缓存范围必须收窄到 /build/assets/——文章图片是同 URL 可变内容,绝不能进这个 location。
两次访问的资源流转对比
第一次访问
────────────────────────────────────────────
浏览器 ──HTML──► nginx ──► Go (no-cache,每次回源)
浏览器 ──JS/CSS─► nginx ──► Go (gzip 传输,约 127KB)
第二次访问
────────────────────────────────────────────
浏览器 ──HTML──► nginx ──► Go (3.7KB)
浏览器 ──JS/CSS─► [本地缓存 immutable] (0 请求,0 字节)
对 315KB/s 的小带宽服务器来说,让回头客"零下载"比把首次下载再压快一点更值钱。
发布与部署
改动走完整的发布链路:
代码提交 (git)
│ 27e9cc2 perf(frontend): Vue 全按需加载
│ e09d0e6 deploy: nginx 优化配置 + compose 调整
▼
Docker 三阶段构建 (Vite 构建 → Go 静态编译 → Alpine 运行时)
│ 前端阶段重建,Go 阶段命中 BuildKit 缓存
▼
推送腾讯云 CCR: qinblog:2026-08-09-v2 / latest
▼
服务器: docker compose pull + up -d qinblog + nginx -s reload
│ storage 用命名卷,镜像升级零数据迁移
▼
线上测速验收
最终结果
部署后复测,全部达标:
指标 优化前 优化后
═══════════════════════════════════════════════════════
首页 JS (gzip) 1.73MB 58KB ▼97%
首屏传输合计 ≈1.8MB ≈127KB ▼93%
HTML TTFB 190~250ms 158~172ms
TLS 握手 偶发 3.4~4.9s ~100ms 稳定
首屏下载耗时(315KB/s) ≈5.7s ≈0.4s
二次访问静态资源 重复下载 本地缓存 0 请求
静态资源缓存头 无 immutable 一年
8080 端口公网暴露 是 否
首次访问从"转圈 6 秒"变成"基本无感",回头客秒开。 折腾完这一轮,博客终于配得上“Go 重构”时承诺的“轻、快”了。
评论 (0)
暂无评论,来抢沙发吧~