先量化,再优化

重装之后顺手给站点做了一次体检,四个数字最扎眼:

指标 数值
HTTPS 只有 HTTP/1.1
gzip 只压 HTML,CSS/JS 全裸奔
图片缓存头 没有
图床里没人引用的死图 40 个,占 6.5MB / 8.9MB

前三个都是 nginx 一行配置的事,最后一个是长期积累的垃圾。

HTTP/2:一个连接跑完所有资源

HTTP/1.1 的问题是「一个连接同时只能处理一个请求」,浏览器只能靠开 6 个并发连接硬堆。页面有几十个资源的时候,排队是实打实的延迟。

开启只需要在 listen 后面加一个词:

listen 443 ssl http2;

验证别只看配置,要实际协商:

echo | openssl s_client -connect 127.0.0.1:443 -servername 你的域名 -alpn h2 2>/dev/null \
| grep -i "ALPN protocol"

看到 ALPN protocol: h2 才算真的开了。

gzip:默认只压 HTML

这是最容易忽略的一项。nginx 的 gzip on; 打开之后,gzip_types 的**默认值只有 text/html**——CSS、JS、JSON、XML 一个都不压。

Ubuntu 的默认配置里这一行是注释掉的,很多人以为开了 gzip 就万事大吉:

gzip on;
gzip_vary on;
gzip_comp_level 6;
gzip_min_length 1024;
gzip_types text/plain text/css text/xml application/json application/javascript
application/xml application/xml+rss image/svg+xml;

验证:

curl -s -o /dev/null -H "Accept-Encoding: gzip" \
-w "%{size_download} bytes\n" https://你的域名/css/xxx.css
curl -sI -H "Accept-Encoding: gzip" https://你的域名/css/xxx.css | grep -i content-encoding

我这个站上的 CSS 压完大概剩三成。

缓存头:让重复访问变成零请求

图片走 nginx 反代到对象存储,响应里只有 ETag 和 Last-Modified,没有 Cache-Control——意味着浏览器每次都要回源确认一遍。图多的页面,读者翻两页就能感觉到。

location ^~ /cykblog/ {
proxy_pass http://127.0.0.1:9000;
add_header Cache-Control "public, max-age=2592000";
}

静态资源同理:

location ~* \.(?:css|js|jpg|jpeg|png|gif|webp|svg|ico|woff2?|ttf|eot)$ {
expires 30d;
add_header Cache-Control "public";
try_files $uri =404;
}

注意一个 add_header 的坑:location 里一旦写了 add_header,server 层的 add_header 就不再继承。所以安全响应头要在每个自己写了 header 的 location 里重复一遍,否则 /cykblog/ 会漏掉。

server_tokens 和安全头

server_tokens off;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Strict-Transport-Security "max-age=2592000" always;

关掉版本号不是为了「安全多少」,是为了别主动告诉扫描器「我是 nginx 1.24.0,你可以去查这个版本的 CVE」。

HSTS 我一开始只给了 30 天,没设 preload。这个头一旦下发,浏览器在有效期内会强制走 HTTPS——证书出问题时网站会直接打不开,所以先短后长比较稳。

第三方 CDN 的两个教训

第一个:图标字体加载失败。 页面里引用了一个国内 CDN 的 iconfont,报 net::ERR_CONNECTION_CLOSED。在服务器上直接抓那个文件是 200,说明 CDN 本身没问题,是我本地到它的链路被掐了。

解决办法不是换 CDN,是把字体搬到自己的域名下:

curl -o iconfont.css "https://原CDN/iconfont.css"
curl -o iconfont.woff2 "https://原CDN/iconfont.woff2"

CSS 里用的是相对路径(url('iconfont.woff2')),放同一个目录就能自动找到,不用改。

第二个:统计脚本报 ERR_BLOCKED_BY_ORB。 一个免费访问量统计服务,请求失败后还会挂两分钟才超时,直接把首屏拖慢。关掉,换成 Cloudflare 的 Web Analytics(无 cookie、脚本小、国内可达)。

图床清理:40 个死图占 73% 空间

把桶里所有对象和「页面里实际引用到的」做了一次差集,结果很难看:

对象总数 80,被引用 51,无人引用 40,占用 6.5MB / 8.9MB

最大的一个是一张 2560×1440 的 PNG 壁纸,4MB——而且同一张图我早就转过 webp(104KB),只是忘了删 PNG,也没在用。

清理时踩了一个坑:用文件名直接 grep HTML 会漏。中文名和带反引号的文件名在 HTML 里是 URL 编码的:

磁盘:`scroll-view`滑动布局.webp
HTML:%60scroll-view%60%E6%BB%91%E5%8A%A8%E5%B8%83%E5%B1%80.webp

第一次按文件名匹配,把它们当成「无人引用」删了,结果 12 张正在用的图直接 404。还好删之前打了包,全部恢复。

正确的做法是先解码再比对,而且要同时扫 html / css / js 三类文件:

for m in re.findall(r'cykblog/([^"\'\s)<>?#]+)', txt):
refs.add(urllib.parse.unquote(m))

改完之后桶从 8.9MB 降到 3.0MB,引用完整性校验为 0 缺失。

上传链路:把规范写进工具

以前传图是「拖进去就完事」,于是文件名长这样:

image-20240729225711008.png
wallhaven-6d61g4_2560x1440.png
`swiper`纵向轮播1.webp

现在用 PicGo 传,把规则配死:

  • 路径:posts/{年}/{月}/(对象存储里的目录)
  • 文件名:{y}{m}{d}-{rand:6},例如 20261027-a3f9c2.webp

有个细节值得记一下:PicGo 的 MinIO 插件里,pathFormat 只支持 {bucket} 一个占位符,**不支持 {年}/{月}**。如果照着别的插件文档填,最后会生成一个字面量路径 posts/{年}/{月}/,对象存的路径和返回的 URL 对不上,图片必然 404。

效果

改动都不大,但叠加起来:

  • 资源请求从「串行排队」变成「一个连接并发」
  • CSS/JS 传输量降到三成左右
  • 重复访问的图片基本不再回源
  • 图床体积减少 66%
  • 首屏少了一个会挂两分钟的第三方请求

都是教科书里的东西,只是以前觉得「博客而已,无所谓」。