静态博客的性能从哪来:缓存与首屏优化

「静态站点快」是一句正确的废话。真正决定一个静态博客打开速度的,不是它有没有后端,而是缓存策略、资源分布和请求链路这三件事。这篇文章把这次迁移中做过的优化和实测数据记录下来,尽量让每个结论都能对应到一条具体的配置。

一、先把测量做对

优化之前,我需要一个可重复的基线。用的命令很简单:

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 之后再验证一遍,是最省事的做法。

七、小结

静态博客的性能优势不是自动获得的,它来自三个具体动作:

  1. 让浏览器缓存住不该变的东西;
  2. 把外部依赖收回到自己域名下;
  3. 让服务端只做「找到文件并返回」这一件事。

这三件事都不复杂,但需要真的去量、去改、去验证。做完之后,一个 2 GB 内存的小机器,也能稳定地在几十毫秒内返回页面。


静态博客的性能从哪来:缓存与首屏优化
https://stone.us.kg/posts/static-blog-performance
作者
Mark Luo
发布于
2026年11月04日
许可协议