首页 JS 从 1.73MB 到 58KB:一次博客访问速度优化复盘

一次例行测速,发现博客首屏要 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 拆包,重依赖全按需加载

思路一句话:谁用谁加载,入口只留地基

改动清单

  1. highlight.js:不再 import hljs from 'highlight.js'(全语言包),新建 hljs-loader.js,页面存在代码块时才动态加载 core + 24 种常用语言;
  2. EasyMDE / inline-attachment / 三个表单组件:从入口静态引入改为动态 import,仅当页面出现对应组件标签时才加载;
  3. 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)

暂无评论,来抢沙发吧~

登录 后发表评论