最近看自己的静态博客时,我发现一个很典型的问题:页面本身打开还行,但一滚到带图的文章,加载就开始拖。
这和源站带宽关系很大。个人博客常用的小带宽云服务器,跑 HTML 没什么压力,一遇到高清截图和架构图就吃紧。图片稍微大一点,浏览器就开始一张张等。
所以这次我不想只把问题归到“服务器太小”。静态博客的性能优化,可以按一个更实际的顺序来做:先让图片变小,再让图片从更近的地方读,最后处理缓存更新带来的副作用。
这篇文章就按这个顺序记录。场景很普通:内容写在 Markdown 里,构建成静态站点后部署到云服务器,Nginx 对外提供 HTML、CSS、JS 和图片。
先看慢在哪里
一个静态博客的发布链路通常很朴素:
- Markdown 写在内容仓库里。
- 构建时把文章和图片复制到静态站点目录。
- Nginx 或其他 Web Server 对外提供 HTML、CSS、JS 和图片。
- 用户访问文章时,图片也从这台源站服务器读取。
这个模型没问题,甚至很适合个人站点。麻烦出在图片请求上。
HTML 文档可能只有几十 KB,一篇文章里的截图却可能轻松到几 MB。读者离源站远一点,首包和下载时间都会变长;同一篇文章被集中转发,源站带宽也会被图片流量吃掉。最后表现出来就是:页面骨架出来了,正文也出来了,图片还在一张张等。
要解决这个问题,我会先做两件事:减少每张图的体积,减少每次访问都打到源站的概率。
第一步:把图片压小
图片压缩是最便宜的一步。
CDN 解决的是“从哪里读”的问题,图片压缩解决的是“读多少”的问题。如果一张截图原来有 8MB,哪怕后面接了 CDN,第一次回源、边缘缓存、用户下载都还是围着这 8MB 转。先把它压到几百 KB,再交给 CDN 分发,源站压力和用户等待时间都会一起降下来。
我更倾向在构建阶段自动处理,少做手动压图。原始图片继续保留在内容仓库里,方便以后重新生成;发布产物里只放适合网页展示的版本。比如可以按这个策略做:
png/jpg/jpeg统一转成 WebP。- 最大宽度限制在
1600px左右,小图不放大。 - WebP 质量先设在
80-85之间,博客截图和说明图通常够用。 gif/svg/webp先原样保留,避免动图丢失或 SVG 被错误栅格化。- Markdown 里仍然按原来的图片路径写,构建时再把最终引用改成
.webp。
这样写文章的时候不用多一步手工处理,发布时也不会把原始大图直接丢到服务器上。这次我把已有构建产物里的 18 张图片做了一轮 WebP 转换,原始体积大约 74.6MB,输出后约 2.1MB。这个数字只代表这次站点里的图片情况,但足够说明个人博客里“先压图”有多划算。
还有一个小细节:不要为了省事去覆盖原图文件。原图是素材,WebP 是发布产物,两者分开,后续要调整质量、尺寸或换格式时会轻松很多。
第二步:给整站接 CDN
图片变小以后,再给站点接 CDN。
对个人博客来说,第一步可以很简单:直接把博客主域名接入 CDN。假设博客域名是 blog.example.com,源站是一台云服务器,或者有一个只给回源使用的 origin.example.com,配置思路大概是这样:
- 在 CDN 控制台添加加速域名
blog.example.com。 - 业务类型选择“网页小文件”,静态博客基本就是这类资源。
- 源站填写服务器公网 IP,或者填写
origin.example.com。 - 回源协议优先用 HTTPS;源站暂时没配证书时,也可以先 HTTP 回源。
- 给 CDN 域名配置 HTTPS 证书。
- DNS 里把
blog.example.com从原来的 A 记录切到 CDN 分配的 CNAME。
这里我会尽量把“用户访问域名”和“CDN 回源地址”分开。blog.example.com 给读者访问,origin.example.com 只给 CDN 回源用。这样出问题时也好判断:访问 CDN 有问题,还是源站本身已经拿不到文件。
如果只是为了让图片快一点,不需要一开始就把图片迁到对象存储。整站 CDN 的改造成本低,文章 Markdown、构建脚本、图片引用路径基本都不用改,收益却很直接。
第三步:缓存规则别一刀切
CDN 接上以后,后面体验好不好,主要看缓存规则。
静态博客里的资源更新频率差异很大。HTML 变动比较敏感,首页、归档页、文章页都可能因为一次发布发生变化;图片则稳定得多,尤其是文章里已经发布过的截图和架构图,通常很少覆盖。
我会按资源类型拆开配置:
| 资源 | 推荐缓存 | 原因 |
|---|---|---|
| HTML | 不缓存,或 1-5 分钟 | 首页和文章更新后要尽快可见 |
图片目录,例如 /uploads/posts/ | 30 天或更长 | 图片最占流量,缓存收益最大 |
图片后缀,例如 webp/jpg/png/gif/svg/avif | 30 天或更长 | 减少重复回源 |
| 带 hash 的 CSS/JS | 30 天到 1 年 | 文件名变化已经代表版本变化 |
| RSS、sitemap | 5-30 分钟 | 更新不频繁,但没必要长时间缓存 |
这里最值得改的是图片发布习惯:如果图片内容变了,尽量改文件名,不要覆盖原文件。
比如 architecture.png 已经被 CDN 缓存了,你后来直接覆盖同名文件,就要考虑 CDN 刷新、浏览器缓存和排查成本。把它改成 architecture-v2.png,问题会少很多。文件名变化以后,新文章引用的是新地址,旧缓存继续留着也不会影响读者看到新图。
源站侧可以补一层响应头,作为默认兜底:
location /uploads/posts/ {
add_header Cache-Control "public, max-age=2592000";
}
location ~* \.html$ {
add_header Cache-Control "no-cache";
}
不过这段配置不用照抄。很多时候直接在 CDN 控制台按目录和后缀配置就够了。只有当你希望“CDN 遵循源站 Cache-Control”时,源站响应头才会变得更关键。
第四步:发布后再刷新缓存
CDN 会让读取变快,也会带来一个新问题:旧内容可能还在边缘节点上。
我会把发布分成两种情况:
- 新增文章、新增图片:通常不需要刷新,第一次访问时 CDN 会自动回源。
- 覆盖同名文件:需要刷新对应 URL,必要时刷新那篇文章的图片目录。
刷新动作要放在部署完成之后。顺序反过来没有意义:如果先刷新,CDN 下一次回源时源站还是旧文件,边缘节点只会重新缓存旧内容。
一个比较稳的发布顺序是:
- 构建静态站点。
- 上传到服务器目录。
- 验证源站文件已经能访问。
- 对覆盖过的 HTML 或图片 URL 做 CDN 刷新。
- 对关键页面和大图做预热。
- 从公网域名再访问一次。
个人博客偶尔全站刷新问题不大,但我不建议把它当成默认动作。全站刷新会让大量请求重新回源,访问量上来以后反而给源站制造压力。能按路径刷新,就尽量按路径刷新。
图片很多以后,再考虑 COS
整站 CDN 的限制也很清楚:源站还是那台云服务器,只是大部分读取被 CDN 缓存挡住了。
当图片数量明显变多,或者图片流量已经远高于页面流量时,可以把图片单独拆到对象存储。腾讯云上常见的做法是 COS + CDN:
- 建一个 COS 存储桶,专门放博客图片。
- 绑定
img.example.com这样的图片域名,并开启 CDN 加速。 - 构建或部署时,把图片同步到 COS。
- 让文章渲染后的图片地址变成
https://img.example.com/posts/...。 - 主站继续部署 HTML,图片流量交给 COS + CDN。
我不会在一开始就做这一步。它会引入对象存储权限、跨域、图片同步、URL 重写、缓存刷新、费用拆分等一串新问题。博客规模还小的时候,整站 CDN 已经能解决最明显的慢。
等到图片真的成了主要流量,再把它拆出来,收益和复杂度才匹配。
我的优化顺序
如果现在从零给一个个人技术博客做性能优化,我会按这个顺序做:
- 先压缩图片,能用 WebP 就用 WebP,截图不要超过文章展示需要的尺寸。
- 主站接入 CDN。
- 图片目录和图片后缀设 30 天以上缓存。
- HTML 保持短缓存,避免文章更新后读者一直看到旧页面。
- 发布脚本里只刷新变更路径。
- 图片规模上来后,再迁到 COS + CDN。
这个顺序的好处是每一步都能单独验证。先看构建产物大小有没有降下来,再看图片响应头和 CDN 命中率,最后看源站带宽有没有下降。
我最不建议的是一上来就把问题做重。图片慢,先让图片变小;源站压力大,再让读者从更近的 CDN 节点读;图片真的成了主要流量,再看要不要拆对象存储。这个顺序通常比直接升级云服务器更划算。