静态博客的性能从哪来:缓存与首屏优化
「静态站点快」是一句正确的废话。真正决定一个静态博客打开速度的,不是它有没有后端,而是缓存策略、资源分布和请求链路这三件事。这篇文章把这次迁移中做过的优化和实测数据记录下来,尽量让每个结论都能对应到一条具体的配置。
一、先把测量做对
优化之前,我需要一个可重复的基线。用的命令很简单:
curl -s -o /dev/null -w \
'dns=%{time_namelookup} connect=%{time_connect} tls=%{time_appconnect} ttfb=%{time_starttransfer} total=%{time_total}\n' \
https://stone.us.kg/
关注两个数:ttfb(首字节时间)和 total(完整下载)。前者反映服务端处理速度,后者还包含传输和体积。
改动前的基线上,TTFB 在 1.8 s 到 3.8 s 之间波动,偶尔直接超时。而同一台机器、同一个 Nginx,返回一个静态文件的 TTFB 是几十毫秒。差距不在网络,在「请求到达之后做了什么」。
二、缓存:最便宜的一次优化
旧站返回的响应头是这样的:
Cache-Control: no-cache, no-store
这意味着浏览器每次都要完整回源,CDN 就算接了也缓存不了。静态站点的优势在这里才真正体现出来——因为内容在构建时就固定了,完全可以放心地让浏览器多存一会儿。
新站的策略分两类:
# 带哈希或极少变化的静态资源:30 天
location ~* \.(?:css|js|woff2?|ttf|svg|png|jpe?g|gif|webp|ico|mp3|mp4)$ {
expires 30d;
try_files $uri =404;
}
# HTML:10 分钟,兼顾更新及时性
location ~* \.html$ {
expires 10m;
try_files $uri =404;
}
这里有个取舍:HTML 缓存越久,回访越快;但文章更新后,读者看到旧版本的时间也越长。 10 分钟是我认为合适的平衡点——发布后最多十分钟,所有回访者都能看到新内容;而对大多数读者来说,这十分钟里他们本来也不会刷新。
如果以后接 CDN,可以把静态资源改成 immutable,并在构建时为文件名加哈希,让缓存策略更激进。
三、资源本地化:减少五次握手
加载一个页面时,浏览器要访问的外部域名越少越好。旧站除了自身域名,还要访问公共 CDN 上的 jQuery、Bootstrap、图标字体和评论组件。每个新域名都意味着一轮 DNS 查询和 TLS 握手,在移动网络下这些开销不可忽略。
这次把主题依赖全部放到了站内:
# _config.fluid.yml
static_prefix:
jquery: /lib/jquery/3.6.4/
bootstrap: /lib/bootstrap/4.6.1/
fancybox: /lib/fancybox/3.5.7/
nprogress: /lib/nprogress/0.2.0/
# ...
图标字体也做了同样的处理:把 CSS 和 woff2/woff/ttf 一起下载到 source/lib/iconfont/,并把 @font-face 里的地址改成本地相对路径。
@font-face {
font-family: "iconfont";
src: url('./font_1736178_k526ubmyhba.woff2') format('woff2'),
url('./font_1736178_k526ubmyhba.woff') format('woff');
}
这样做的另一个好处是版本可控。公共 CDN 上的地址随时可能变化,甚至失效;放在站内的文件,只要我不动,它就一直在。
注意:本地化不是把依赖复制过来就完了,还要确认构建产物里真的没有残留的外部地址。我的检查方式是直接扫生成的 HTML:
bash grep -rhoE "https?://[a-zA-Z0-9.-]+" public --include=*.html | sort | uniq -c
四、服务端:让 Nginx 少做一点
静态站点的服务端配置其实很短,关键是别让它做多余的事:
root /www/sites/stone.us.kg/static;
index index.html;
location / {
try_files $uri $uri/ $uri/index.html =404;
}
try_files 的顺序对应 Hexo 的目录式输出:/posts/foo/ 会命中目录,再由 index 指令找到 index.html;不带斜杠的 /posts/foo 会自动 301 到带斜杠的地址。
另外几项是标配,但值得确认它们真的开着:
- gzip:HTML、CSS、JS 文本压缩后通常只有原来的 20% – 30%;
- HTTP/2:多路复用,减少并发请求的排队;
- 安全响应头:
X-Content-Type-Options、Referrer-Policy这类头部不提升速度,但不该缺。
五、实测结果
同一台服务器、同一个域名,迁移前后的对比:
| 指标 | 迁移前 | 迁移后 |
|---|---|---|
| 首页 TTFB | 1.8 – 3.8 s | 20 – 50 ms |
| HTML 缓存 | no-store |
max-age=600 |
| 静态资源缓存 | 无 | max-age=2592000 |
| 外部资源域名 | 多个公共 CDN | 0 |
| 首屏阻塞请求 | 动态渲染 + 外部 CSS | 同域 CSS,无阻塞 JS |
需要说明的是,这些数字来自我自己的服务器和网络环境,不代表所有人的体验。但趋势是稳定的:每次优化都对应一个可测量的指标,而不是「感觉快了」。
六、几个容易踩的坑
1. expires 和 add_header 会互相影响。 Nginx 里,只要某个 location 出现了 add_header,父级的 add_header 就不再继承。如果安全头写在 server 块,缓存头写在 location 块,很容易出现「某些页面少了安全头」的情况。我的处理是缓存只用 expires,安全头统一放 server 块。
2. 带查询参数的 URL 不会被缓存命中。 静态文件的缓存键包含完整 URL,app.js?v=1 和 app.js?v=2 是两个条目。如果更新频繁,要么改文件名,要么接受缓存失效。
3. 本地化之后要重新构建并检查产物。 配置文件改了不等于 HTML 改了,hexo clean && hexo generate 之后再验证一遍,是最省事的做法。
七、小结
静态博客的性能优势不是自动获得的,它来自三个具体动作:
- 让浏览器缓存住不该变的东西;
- 把外部依赖收回到自己域名下;
- 让服务端只做「找到文件并返回」这一件事。
这三件事都不复杂,但需要真的去量、去改、去验证。做完之后,一个 2 GB 内存的小机器,也能稳定地在几十毫秒内返回页面。